diff --git a/.gitignore b/.gitignore index 263fd53e..5ea82a7f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ testem.log Thumbs.db src/environments/environment.ts -src/environments/environment.ci.ts \ No newline at end of file +src/environments/environment.ci.ts +/tsconfig.spec.agent-*.json +/coverage diff --git a/angular.json b/angular.json index ef39285c..f314ae60 100644 --- a/angular.json +++ b/angular.json @@ -148,7 +148,17 @@ "@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.css" ], - "scripts": [] + "scripts": [], + "karmaConfig": "karma.conf.js", + "codeCoverageExclude": [ + "Common-UI/**", + "src/testing/**", + "src/environments/**", + "src/test.ts", + "src/polyfills.ts", + "src/main.ts", + "**/mocks/**" + ] } }, "lint": { diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..3a20a586 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,50 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + jasmine: { + random: false, + timeoutInterval: 10000, + }, + captureConsole: false, + }, + jasmineHtmlReporter: { + suppressAll: true, + }, + coverageReporter: { + dir: process.env.COVERAGE_DIR || require('path').join(__dirname, './coverage/mmu-ui'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'lcovonly' }, + { type: 'json-summary' }, + { type: 'text-summary' }, + ], + }, + reporters: ['progress', 'kjhtml'], + browsers: ['ChromeHeadlessCI'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], + }, + }, + browserNoActivityTimeout: 300000, + browserDisconnectTimeout: 60000, + browserDisconnectTolerance: 2, + captureTimeout: 300000, + singleRun: true, + restartOnFileChange: false, + }); +}; diff --git a/package.json b/package.json index 1bf0e732..fc9bd074 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", + "test:ci": "(test -f src/environments/environment.ts || cp src/environments/environment.test.ts src/environments/environment.ts) && ng test --watch=false --code-coverage", "lint": "ng lint", "commit": "cz", "lint:fix": "ng lint --fix", diff --git a/src/app/app-modules/captcha-service/captcha.service.spec.ts b/src/app/app-modules/captcha-service/captcha.service.spec.ts new file mode 100644 index 00000000..788f99ff --- /dev/null +++ b/src/app/app-modules/captcha-service/captcha.service.spec.ts @@ -0,0 +1,183 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { CaptchaService } from './captcha.service'; +import { environment } from 'src/environments/environment'; + +describe('CaptchaService', () => { + let service: CaptchaService; + let originalAppendChild: (node: T) => T; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [CaptchaService], + }); + service = TestBed.inject(CaptchaService); + originalAppendChild = document.head.appendChild.bind(document.head); + }); + + afterEach(() => { + // Clean up any script elements added during tests + const scripts = document.head.querySelectorAll('script'); + scripts.forEach(script => { + if ( + script.src && + script.src.includes(environment.captchaChallengeURL || 'captcha') + ) { + script.remove(); + } + }); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#loadScript', () => { + it('should return a Promise', async () => { + let added: any; + spyOn(document.head, 'appendChild').and.callFake((node: any) => { + added = node; + return node; + }); + const result = service.loadScript(); + expect(result).toBeInstanceOf(Promise); + added.onerror(new Event('error')); + await expectAsync(result).toBeRejected(); + }); + + it('should append a script element to document.head', async () => { + const appendChildSpy = spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onload(new Event('load')); + return node; + } + ); + + await service.loadScript(); + expect(appendChildSpy).toHaveBeenCalled(); + }); + + it('should set script src to environment.captchaChallengeURL', async () => { + let capturedScript: HTMLScriptElement | null = null; + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + capturedScript = node as any; + (capturedScript as any).onload(new Event('load')); + return node; + } + ); + + await service.loadScript(); + expect(capturedScript!.src).toContain(environment.captchaChallengeURL); + }); + + it('should set async and defer on the script element', async () => { + let capturedScript: HTMLScriptElement | null = null; + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + capturedScript = node as any; + (capturedScript as any).onload(new Event('load')); + return node; + } + ); + + await service.loadScript(); + expect(capturedScript!.async).toBe(true); + expect(capturedScript!.defer).toBe(true); + }); + + it('should resolve when script loads successfully', async () => { + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onload(new Event('load')); + return node; + } + ); + + await expectAsync(service.loadScript()).toBeResolved(); + }); + + it('should set scriptLoaded to true after successful load', async () => { + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onload(new Event('load')); + return node; + } + ); + + await service.loadScript(); + // Verify by calling again - it should resolve immediately without appending + const appendChildSpy = document.head.appendChild as jasmine.Spy; + appendChildSpy.calls.reset(); + + await service.loadScript(); + expect(appendChildSpy).not.toHaveBeenCalled(); + }); + + it('should resolve immediately if script is already loaded', async () => { + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onload(new Event('load')); + return node; + } + ); + + await service.loadScript(); + const appendChildSpy = document.head.appendChild as jasmine.Spy; + appendChildSpy.calls.reset(); + + await service.loadScript(); + expect(appendChildSpy).not.toHaveBeenCalled(); + }); + + it('should reject with Error when script fails to load', async () => { + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onerror(new Event('error')); + return node; + } + ); + + await expectAsync(service.loadScript()).toBeRejectedWithError( + /Failed to load CAPTCHA script/ + ); + }); + + it('should include the captchaChallengeURL in the error message', async () => { + spyOn(document.head, 'appendChild').and.callFake( + (node: T): T => { + (node as any).onerror(new Event('error')); + return node; + } + ); + + try { + await service.loadScript(); + fail('Expected an error to be thrown'); + } catch (e: any) { + expect(e.message).toContain(environment.captchaChallengeURL); + } + }); + }); +}); diff --git a/src/app/app-modules/captcha/captcha.component.spec.ts b/src/app/app-modules/captcha/captcha.component.spec.ts new file mode 100644 index 00000000..dc59ac72 --- /dev/null +++ b/src/app/app-modules/captcha/captcha.component.spec.ts @@ -0,0 +1,121 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CaptchaComponent } from './captcha.component'; +import { CaptchaService } from '../captcha-service/captcha.service'; +import { environment } from 'src/environments/environment'; +import { NO_ERRORS_SCHEMA } from 'src/testing/test-utils'; + +describe('CaptchaComponent', () => { + let component: CaptchaComponent; + let fixture: ComponentFixture; + let captchaService: { loadScript: jasmine.Spy }; + let turnstileMock: any; + let hadTurnstile: boolean; + let previousTurnstile: any; + + beforeEach(async () => { + hadTurnstile = 'turnstile' in window; + previousTurnstile = (window as any).turnstile; + turnstileMock = { + render: jasmine.createSpy('render').and.returnValue('widget-1'), + reset: jasmine.createSpy('reset'), + remove: jasmine.createSpy('remove'), + }; + (window as any).turnstile = turnstileMock; + captchaService = { + loadScript: jasmine.createSpy('loadScript').and.resolveTo(undefined), + }; + + await TestBed.configureTestingModule({ + declarations: [CaptchaComponent], + providers: [{ provide: CaptchaService, useValue: captchaService }], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(CaptchaComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + if (hadTurnstile) (window as any).turnstile = previousTurnstile; + else delete (window as any).turnstile; + }); + + it('loads the script and renders the widget into the container', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + await component.ngAfterViewInit(); + expect(captchaService.loadScript).toHaveBeenCalled(); + expect(turnstileMock.render).toHaveBeenCalledTimes(1); + const [el, opts] = turnstileMock.render.calls.mostRecent().args; + expect(el.id).toBe('cf-turnstile'); + expect(opts.sitekey).toBe(environment.siteKey); + expect(opts.theme).toBe('light'); + }); + + it('emits the token from the widget callback', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const emitted: string[] = []; + component.tokenResolved.subscribe(t => emitted.push(t)); + turnstileMock.render.calls.mostRecent().args[1].callback('tok-123'); + expect(emitted).toEqual(['tok-123']); + }); + + it('does not render when the container is missing', async () => { + component.captchaRef = undefined as any; + await component.ngAfterViewInit(); + expect(turnstileMock.render).not.toHaveBeenCalled(); + }); + + it('swallows script loading errors', async () => { + captchaService.loadScript.and.rejectWith(new Error('fail')); + fixture.detectChanges(); + await expectAsync(component.ngAfterViewInit()).toBeResolved(); + expect(turnstileMock.render).not.toHaveBeenCalled(); + }); + + it('reset() and ngOnDestroy() act on the rendered widget', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + component.reset(); + expect(turnstileMock.reset).toHaveBeenCalledWith('widget-1'); + component.ngOnDestroy(); + expect(turnstileMock.remove).toHaveBeenCalledWith('widget-1'); + }); + + it('reset() and ngOnDestroy() do nothing before a widget exists', () => { + component.reset(); + component.ngOnDestroy(); + expect(turnstileMock.reset).not.toHaveBeenCalled(); + expect(turnstileMock.remove).not.toHaveBeenCalled(); + }); + + it('ngOnDestroy() skips removal when turnstile has no remove()', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + delete turnstileMock.remove; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); +}); diff --git a/src/app/app-modules/core/components/allergen-search/allergen-search.component.spec.ts b/src/app/app-modules/core/components/allergen-search/allergen-search.component.spec.ts index e9a9b0f6..0eea4ff3 100644 --- a/src/app/app-modules/core/components/allergen-search/allergen-search.component.spec.ts +++ b/src/app/app-modules/core/components/allergen-search/allergen-search.component.spec.ts @@ -19,3 +19,105 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { MasterdataService } from 'src/app/app-modules/nurse-doctor/shared/services'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { AllergenSearchComponent } from './allergen-search.component'; + +describe('AllergenSearchComponent', () => { + let fixture: ComponentFixture; + let component: AllergenSearchComponent; + let master: any; + let ref: any; + const rows = [ + { conceptID: '1', term: 'Peanut' }, + { conceptID: '2', term: 'Pollen' }, + ]; + + const create = async (term: string, response: any) => { + master = autoSpy(MasterdataService); + master.searchDiagnosisBasedOnPageNo1.and.returnValue(response); + await TestBed.configureTestingModule({ + declarations: [AllergenSearchComponent], + providers: [ + ...commonTestProviders({ dialogData: { searchTerm: term } }), + { provide: MasterdataService, useValue: master }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(AllergenSearchComponent, '') + .compileComponents(); + ref = TestBed.inject(MatDialogRef); + fixture = TestBed.createComponent(AllergenSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }; + + afterEach(() => fixture.destroy()); + + it('searches the dialog term on init and fills the table', async () => { + await create('pea', of({ statusCode: 200, data: { sctMaster: rows } })); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(master.searchDiagnosisBasedOnPageNo1).toHaveBeenCalledWith('pea', 0); + expect(component.components.data).toEqual(rows); + expect(component.showProgressBar).toBeFalse(); + }); + + it('does not search terms of 2 characters or less', async () => { + await create('pe', of({ statusCode: 200 })); + expect(master.searchDiagnosisBasedOnPageNo1).not.toHaveBeenCalled(); + expect(component.showProgressBar).toBeFalse(); + }); + + it('sets no-record message when result empty', async () => { + await create('xyz', of({ statusCode: 200, data: { sctMaster: [] } })); + expect(component.message).toBe(LANGUAGE_EN.common.noRecordFound); + expect(component.components.data).toEqual([]); + }); + + it('sets no-record message when data is null', async () => { + await create('xyz', of({ statusCode: 200, data: null })); + expect(component.message).toBe(LANGUAGE_EN.common.noRecordFound); + }); + + it('resets on non-200 status', async () => { + await create('xyz', of({ statusCode: 5000 })); + expect(component.components.data).toEqual([]); + expect(component.showProgressBar).toBeFalse(); + }); + + it('resets on error', async () => { + await create('xyz', throwingObs()); + expect(component.components.data).toEqual([]); + expect(component.showProgressBar).toBeFalse(); + }); + + it('selectComponentName stores selection and submit closes with it', async () => { + await create('pea', of({ statusCode: 200, data: { sctMaster: rows } })); + component.selectComponentName('2', 'Pollen'); + expect(component.selectedComponentNo).toBe('2'); + expect(component.selectedComponent).toBe('Pollen'); + expect(component.selectedItem).toBe('Pollen'); + component.submitComponentList(); + expect(ref.close).toHaveBeenCalledWith({ + componentNo: '2', + component: 'Pollen', + }); + }); + + it('ngDoCheck refreshes the language', async () => { + await create('pe', of({})); + component.current_language_set = undefined; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); + }); +}); diff --git a/src/app/app-modules/core/components/app-footer/app-footer.component.spec.ts b/src/app/app-modules/core/components/app-footer/app-footer.component.spec.ts index 2df2a834..ff7805a2 100644 --- a/src/app/app-modules/core/components/app-footer/app-footer.component.spec.ts +++ b/src/app/app-modules/core/components/app-footer/app-footer.component.spec.ts @@ -20,27 +20,93 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpServiceService } from '../../services/http-service.service'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + createHttpServiceMock, +} from 'src/testing/test-utils'; import { AppFooterComponent } from './app-footer.component'; describe('AppFooterComponent', () => { - let component: AppFooterComponent; let fixture: ComponentFixture; + let component: AppFooterComponent; + let http: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + http = createHttpServiceMock(LANGUAGE_EN); + await TestBed.configureTestingModule({ + imports: [RouterTestingModule], declarations: [AppFooterComponent], + providers: [{ provide: HttpServiceService, useValue: http }], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(AppFooterComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + afterEach(() => fixture?.destroy()); + + it('sets language, year and polls online status every second', fakeAsync(() => { + const onLine = spyOnProperty(navigator, 'onLine').and.returnValue(true); + fixture.detectChanges(); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + expect(component.year).toBe(new Date().getFullYear()); + expect(component.status).toBeFalse(); + tick(1000); + expect(component.status).toBeTrue(); + onLine.and.returnValue(false); + tick(1000); + expect(component.status).toBeFalse(); + discardPeriodicTasks(); + })); + + it('renders localized text when language is available', fakeAsync(() => { + spyOnProperty(navigator, 'onLine').and.returnValue(true); + fixture.detectChanges(); + tick(1000); + fixture.detectChanges(); + const text = fixture.nativeElement.textContent; + expect(text).toContain(`${component.year} ©`); + expect(text).toContain(LANGUAGE_EN.online); + const dot = fixture.nativeElement.querySelector( + 'span[style*="border-radius"]' + ) as HTMLElement; + expect(dot).not.toBeNull(); + expect(dot.style.background).toContain('green'); + discardPeriodicTasks(); + })); + + it('renders fallback text when no language is available', fakeAsync(() => { + http.appCurrentLanguge.next(undefined); + spyOnProperty(navigator, 'onLine').and.returnValue(false); + fixture.detectChanges(); + expect(component.currentLanguageSet).toBeUndefined(); + const text = fixture.nativeElement.textContent; + expect(text).toContain('Powered by: WIPRO'); + expect(text).toContain('PSMRI'); + expect(text).toContain('Offline'); + expect( + fixture.nativeElement.querySelector('span[style*="border-radius"]') + ).toBeNull(); + expect(text).toContain('Feedback'); + discardPeriodicTasks(); + })); + + it('ngDoCheck picks up a changed language', fakeAsync(() => { + fixture.detectChanges(); + const other = { online: 'On', offline: 'Off' }; + http.appCurrentLanguge.next(other); + component.ngDoCheck(); + expect(component.currentLanguageSet).toBe(other); + discardPeriodicTasks(); + })); }); diff --git a/src/app/app-modules/core/components/app-header/app-header.component.spec.ts b/src/app/app-modules/core/components/app-header/app-header.component.spec.ts index 03437acf..50e74f0d 100644 --- a/src/app/app-modules/core/components/app-header/app-header.component.spec.ts +++ b/src/app/app-modules/core/components/app-header/app-header.component.spec.ts @@ -21,193 +21,377 @@ */ import { - async, ComponentFixture, - tick, - fakeAsync, TestBed, + fakeAsync, + flush, } from '@angular/core/testing'; -import { AuthService } from '../../app-modules/services/auth.service'; +import { MatDialog } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; +import { Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { AuthService, ConfirmationService } from '../../services'; +import { HttpServiceService } from '../../services/http-service.service'; +import { IotService } from '../../services/iot.service'; +import { IotBluetoothComponent } from '../iot-bluetooth/iot-bluetooth.component'; +import { ShowCommitAndVersionDetailsComponent } from '../show-commit-and-version-details/show-commit-and-version-details.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createHttpServiceMock, + throwingObs, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { AppHeaderComponent } from './app-header.component'; -import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../../core/material.module'; - -class MockActivatedRoute { - snapshot = { - params: [null], - }; -} + +const ROLES = ['Registrar', 'Nurse', 'Doctor', 'Lab Technician', 'Pharmacist']; describe('AppHeaderComponent', () => { - let component: AppHeaderComponent; let fixture: ComponentFixture; - let authService: AuthService; + let component: AppHeaderComponent; + let auth: any; + let http: any; + let disconnect$: BehaviorSubject; + let confirm: any; + let dialog: any; let router: Router; + let sessionSeed: Record; - beforeEach(async(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], - imports: [MaterialModule, NoopAnimationsModule], + const create = ( + opts: { showRoles?: boolean; authenticated?: boolean } = {} + ) => { + if (opts.authenticated) sessionStorage.setItem('isAuthenticated', 'true'); + fixture = TestBed.createComponent(AppHeaderComponent); + component = fixture.componentInstance; + component.showRoles = !!opts.showRoles; + fixture.detectChanges(); + }; + + beforeEach(async () => { + sessionSeed = { + servicePointName: 'barpeta sp', + userName: 'prabhsimran singh', + providerServiceID: 3, + role: JSON.stringify(ROLES), + }; + auth = autoSpy(AuthService); + auth.getUIVersionAndCommitDetails.and.returnValue( + of({ version: '3.0.0', commit: 'uicommit' }) + ); + http = { + ...createHttpServiceMock(), + getLanguage: jasmine + .createSpy('getLanguage') + .and.returnValue(of({ English: LANGUAGE_EN })), + }; + http.fetchLanguageSet.and.returnValue( + of({ data: [{ languageName: 'English' }, { languageName: 'Hindi' }] }) + ); + disconnect$ = new BehaviorSubject(true); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MatMenuModule], declarations: [AppHeaderComponent], providers: [ - { provide: Router, useClass: RouterStub }, - { provide: ActivatedRoute, useClass: MockActivatedRoute }, - { provide: AuthService, useClass: authServiceMock }, + ...commonTestProviders({ session: sessionSeed }), + { provide: HttpServiceService, useValue: http }, + { provide: AuthService, useValue: auth }, + { + provide: IotService, + useValue: { disconnectValue$: disconnect$.asObservable() }, + }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AppHeaderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - component.showRoles = false; - authService = TestBed.get(AuthService); - router = TestBed.get(Router); - window.localStorage.setItem('servicePointName', 'Barpeta SP'); - window.localStorage.setItem('userName', 'Prabhsimran Singh'); - window.sessionStorage.setItem('isAuthenticated', 'true'); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); }); - it('should create app header component', () => { - expect(component).toBeTruthy(); + afterEach(() => { + fixture?.destroy(); + sessionStorage.removeItem('isAuthenticated'); + sessionStorage.removeItem('setLanguage'); }); - it('should get servicePointName from localstorage when init', () => { - fixture.autoDetectChanges(); - expect(component.servicePoint).toEqual( - window.localStorage.getItem('servicePointName') - ); - }); + describe('ngOnInit', () => { + it('reads service point, user name and provider from session storage', () => { + create(); + expect(component.servicePoint).toBe('barpeta sp'); + expect(component.userName).toBe('prabhsimran singh'); + expect(component.status).toBe(3); + const text = fixture.nativeElement.textContent; + expect(text).toContain('Prabhsimran Singh'); + expect(text).toContain('Barpeta Sp'); + }); - it('should get userName from localstorage when init', () => { - fixture.autoDetectChanges(); - expect(component.userName).toEqual(window.localStorage.getItem('userName')); - }); + it('is not authenticated and skips language fetch without the session flag', () => { + create(); + expect(component.isAuthenticated).toBeFalse(); + expect(http.fetchLanguageSet).not.toHaveBeenCalled(); + expect(fixture.nativeElement.querySelector('.logout')).toBeNull(); + }); - it('should get isAuthenticated from localstorage when init', () => { - fixture.autoDetectChanges(); - expect(component.isAuthenticated.toString()).toBe( - window.sessionStorage.getItem('isAuthenticated') - ); - }); + it('loads UI version details', () => { + create(); + expect(auth.getUIVersionAndCommitDetails).toHaveBeenCalledWith( + 'assets/git-version.json' + ); + expect(component.versionUI).toBe('3.0.0'); + }); - it("should keep roles as undefined as we've kept showroles as no", () => { - fixture.autoDetectChanges(); - expect(component.roles).toBe(undefined); - }); + it('tolerates UI version load failure', () => { + auth.getUIVersionAndCommitDetails.and.returnValue(throwingObs()); + create(); + expect(component.versionUI).toBeUndefined(); + }); - it("should keep filteredNavigation as undefined as we've kept showroles as no", () => { - fixture.autoDetectChanges(); - expect(component.filteredNavigation).toBe(undefined); - }); + it('tracks bluetooth connection state', () => { + create(); + expect(component.isConnected).toBeTrue(); + disconnect$.next(false); + expect(component.isConnected).toBeFalse(); + disconnect$.next(true); + disconnect$.next(undefined); + expect(component.isConnected).toBeFalse(); + }); - it('should set roles as per save in localstorage when we have showRoles set to true', () => { - component.showRoles = true; - const role = - '["Registrar","Nurse","Doctor","TC Specialist","Oncologist","Lab Technician","Pharmacist"]'; - window.localStorage.setItem('role', role); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.roles).toBeTruthy(); - }); + it('does not compute roles when showRoles is false', () => { + create(); + expect(component.roles).toBeUndefined(); + expect(component.filteredNavigation).toBeUndefined(); + }); - it("should not set roles as per save in localstorage when we don't have showRoles set to true", () => { - const role = - '["Registrar","Nurse","Doctor","TC Specialist","Oncologist","Lab Technician","Pharmacist"]'; - window.localStorage.setItem('role', role); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.roles).not.toBeTruthy(); - }); + it('filters navigation by stored roles when showRoles is true', () => { + create({ showRoles: true }); + expect(component.roles).toEqual(ROLES); + expect(component.filteredNavigation.map((n: any) => n.role)).toEqual( + ROLES + ); + const buttons = fixture.nativeElement.querySelectorAll('.nav-btn'); + expect(buttons.length).toBe(ROLES.length + 1); + }); - it('should filteredNavigation as per save in localstorage when we have showRoles set to true', () => { - component.showRoles = true; - const role = - '["Registrar","Nurse","Doctor","TC Specialist","Oncologist","Lab Technician","Pharmacist"]'; - window.localStorage.setItem('role', role); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.filteredNavigation).toBeTruthy(); + it('yields empty navigation when no roles stored', () => { + (TestBed.inject(SessionStorageService) as any).store.set('role', '[]'); + create({ showRoles: true }); + expect(component.filteredNavigation).toEqual([]); + }); }); - it('should call logout method on click of logout tag', fakeAsync(() => { - const spy = spyOn(authService, 'logoutUser').and.returnValue( - Observable.of(1) - ); - component.ngOnInit(); - fixture.detectChanges(); - const el = fixture.debugElement.query(By.css('#logoutButton')); - el.triggerEventHandler('click', null); - tick(); - fixture.detectChanges(); - expect(spy).toHaveBeenCalled(); - })); + describe('when authenticated', () => { + it('fetches languages and applies stored/default language', () => { + create({ authenticated: true, showRoles: true }); + expect(component.isAuthenticated).toBeTrue(); + expect(component.languageArray.length).toBe(2); + expect(http.getLanguage).toHaveBeenCalledWith('./assets/English.json'); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + expect(component.app_language).toBe('English'); + expect(sessionStorage.getItem('setLanguage')).toBe('English'); + expect(http.getCurrentLanguage).toHaveBeenCalledWith(LANGUAGE_EN); + expect( + component.filteredNavigation.find((n: any) => n.role === 'Nurse').link + ).toBe('/nurse-doctor/nurse-worklist'); + expect(fixture.nativeElement.querySelector('.logout')).not.toBeNull(); + }); - it('should not clear local storage asresponse is not correct', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 401 }) - ); - component.ngOnInit(); - component.logout(); + it('uses the language saved in session storage', () => { + sessionStorage.setItem('setLanguage', 'Hindi'); + http.getLanguage.and.returnValue(of({ Hindi: { ...LANGUAGE_EN } })); + create({ authenticated: true }); + expect(http.getLanguage).toHaveBeenCalledWith('./assets/Hindi.json'); + expect(component.app_language).toBe('Hindi'); + }); - expect(localStorage.length).not.toEqual(0); + it('ignores language set response without data', () => { + http.fetchLanguageSet.and.returnValue(of({})); + create({ authenticated: true }); + expect(http.getLanguage).not.toHaveBeenCalled(); + }); }); - it('should not clear session storage asresponse is not correct', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 401 }) - ); - component.ngOnInit(); - component.logout(); + describe('changeLanguage / languageSuccessHandler', () => { + beforeEach(() => { + create(); + component.languageArray = [{ languageName: 'English' }]; + }); + + it('alerts with fallback text when the language file is empty', () => { + http.getLanguage.and.returnValue(of(null)); + component.changeLanguage('Tamil'); + expect(confirm.alert).toHaveBeenCalledWith( + 'Selected language is not defined', + 'error' + ); + }); + + it('alerts with localized text when file load fails', () => { + component.currentLanguageSet = LANGUAGE_EN; + http.getLanguage.and.returnValue(throwingObs()); + component.changeLanguage('Tamil'); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.comingUpWithThisLang + ' Tamil', + 'error' + ); + }); + + it('alerts with fallback when file load fails and no language set', () => { + http.getLanguage.and.returnValue(throwingObs()); + component.changeLanguage('Tamil'); + expect(confirm.alert).toHaveBeenCalledWith( + 'Selected language is coming up with Tamil', + 'error' + ); + }); + + it('languageSuccessHandler alerts for undefined response', () => { + component.currentLanguageSet = LANGUAGE_EN; + component.languageSuccessHandler(undefined, 'English'); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.langNotDefinesd, + 'error' + ); + }); - expect(sessionStorage.length).not.toEqual(0); + it('languageSuccessHandler alerts when language key missing', () => { + component.languageSuccessHandler({ Other: {} }, 'Tamil'); + expect(confirm.alert).toHaveBeenCalledWith( + 'Selected language is coming up with Tamil', + 'error' + ); + expect(sessionStorage.getItem('setLanguage')).toBeNull(); + }); + + it('languageSuccessHandler keeps app_language when not in language list', () => { + component.languageSuccessHandler({ Tamil: LANGUAGE_EN }, 'Tamil'); + expect(component.app_language).toBe('English'); + expect(sessionStorage.getItem('setLanguage')).toBe('Tamil'); + }); + + it('languageSuccessHandler with null language object: sets app_language then fails role navigation', () => { + expect(() => + component.languageSuccessHandler({ Tamil: null }, 'Tamil') + ).toThrowError(TypeError); + expect(component.app_language).toBe('Tamil'); + }); + + it('selecting a language in the dropdown calls changeLanguage', () => { + component.languageArray = [ + { languageName: 'English' }, + { languageName: 'Hindi' }, + ]; + fixture.detectChanges(); + const spy = spyOn(component, 'changeLanguage'); + const select: HTMLSelectElement = + fixture.nativeElement.querySelector('select'); + select.value = 'Hindi'; + select.dispatchEvent(new Event('change')); + expect(spy).toHaveBeenCalledWith('Hindi'); + }); }); - it('should clear local storage as response is correct', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 200 }) - ); - component.ngOnInit(); - component.logout(); + it('rolenavigation without showRoles leaves filteredNavigation unset', () => { + create(); + component.currentLanguageSet = LANGUAGE_EN; + component.rolenavigation(); + expect(component.navigation.length).toBe(8); + expect(component.navigation[7].label).toBe(LANGUAGE_EN.common.dataSync); + expect(component.filteredNavigation).toBeUndefined(); + }); - expect(localStorage.length).toEqual(0); + it('rolenavigation with showRoles and no stored roles keeps previous filter', () => { + create({ showRoles: true }); + const previous = component.filteredNavigation; + const ss: any = component.sessionstorage; + ss.store.delete('role'); + component.currentLanguageSet = LANGUAGE_EN; + component.rolenavigation(); + expect(component.roles).toBeNull(); + expect(component.filteredNavigation).toBe(previous); }); - it('should clear local storage as response is correct', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 200 }) - ); - component.ngOnInit(); - component.logout(); + it('DataSync navigates to the data sync module', () => { + create(); + component.DataSync(); + expect(router.navigate).toHaveBeenCalledWith(['/datasync']); + }); - expect(sessionStorage.length).toEqual(0); + describe('logout', () => { + it('logs out, navigates to login, resets language and clears session storage', fakeAsync(() => { + create({ authenticated: true }); + http.getLanguage.calls.reset(); + sessionStorage.setItem('foo', 'bar'); + (fixture.nativeElement.querySelector('.logout a') as HTMLElement).click(); + flush(); + expect(auth.logout).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/login']); + expect(http.getLanguage).toHaveBeenCalledWith('./assets/English.json'); + expect(sessionStorage.getItem('foo')).toBeNull(); + })); + + it('does nothing further when navigation fails', fakeAsync(() => { + create(); + (router.navigate as jasmine.Spy).and.resolveTo(false); + sessionStorage.setItem('foo', 'bar'); + component.logout(); + flush(); + expect(http.getLanguage).not.toHaveBeenCalled(); + expect(sessionStorage.getItem('foo')).toBe('bar'); + sessionStorage.removeItem('foo'); + })); }); - it('should call to navigate to other component', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 200 }) - ); - const spier = spyOn(router, 'navigate'); - component.ngOnInit(); - component.logout(); - expect(spier).toHaveBeenCalled(); + describe('version details', () => { + beforeEach(() => create()); + + it('opens version dialog with API and UI details', () => { + auth.getAPIVersionAndCommitDetails.and.returnValue( + of({ + statusCode: 200, + data: { 'git.build.version': '2.1', 'git.commit.id': 'apicommit' }, + }) + ); + component.showVersionAndCommitDetails(); + expect(dialog.open).toHaveBeenCalledWith( + ShowCommitAndVersionDetailsComponent, + { + data: { + commitDetailsUI: { version: '3.0.0', commit: 'uicommit' }, + commitDetailsAPI: { version: '2.1', commit: 'apicommit' }, + }, + } + ); + }); + + it('uses NA when API details are missing', () => { + component.constructAPIAndUIDetails({}); + expect( + dialog.open.calls.mostRecent().args[1].data.commitDetailsAPI + ).toEqual({ + version: 'NA', + commit: 'NA', + }); + }); + + it('does not open dialog on non-200 or error', () => { + auth.getAPIVersionAndCommitDetails.and.returnValue( + of({ statusCode: 500 }) + ); + component.showVersionAndCommitDetails(); + auth.getAPIVersionAndCommitDetails.and.returnValue(throwingObs()); + component.showVersionAndCommitDetails(); + expect(dialog.open).not.toHaveBeenCalled(); + }); }); - it('should call to navigate to login component', () => { - spyOn(authService, 'logoutUser').and.returnValue( - Observable.of({ statusCode: 200 }) - ); - const spier = spyOn(router, 'navigate'); - component.ngOnInit(); - component.logout(); - expect(spier).toHaveBeenCalledWith(['/login']); + it('openIOT opens the bluetooth dialog from the header icon', () => { + create({ authenticated: true }); + (fixture.nativeElement.querySelector('.iot') as HTMLElement).click(); + expect(dialog.open).toHaveBeenCalledWith(IotBluetoothComponent, { + width: '600px', + }); }); }); diff --git a/src/app/app-modules/core/components/beneficiary-details/beneficiary-details.component.spec.ts b/src/app/app-modules/core/components/beneficiary-details/beneficiary-details.component.spec.ts index 4e418c0c..d99dc0e2 100644 --- a/src/app/app-modules/core/components/beneficiary-details/beneficiary-details.component.spec.ts +++ b/src/app/app-modules/core/components/beneficiary-details/beneficiary-details.component.spec.ts @@ -20,27 +20,109 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Subject, of } from 'rxjs'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { BeneficiaryDetailsService } from '../../services/beneficiary-details.service'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { BeneficiaryDetailsComponent } from './beneficiary-details.component'; describe('BeneficiaryDetailsComponent', () => { - let component: BeneficiaryDetailsComponent; let fixture: ComponentFixture; + let component: BeneficiaryDetailsComponent; + let params: Subject; + let details$: BehaviorSubject; + let benService: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + params = new Subject(); + details$ = new BehaviorSubject(null); + benService = { + beneficiaryDetails$: details$.asObservable(), + getBeneficiaryDetails: jasmine.createSpy('getBeneficiaryDetails'), + getBeneficiaryImage: jasmine + .createSpy('getBeneficiaryImage') + .and.returnValue(of({})), + }; + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [BeneficiaryDetailsComponent], + providers: [ + ...commonTestProviders({ session: { benFlowID: '77' } }), + { provide: ActivatedRoute, useValue: { params } }, + { provide: BeneficiaryDetailsService, useValue: benService }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(BeneficiaryDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('initialises language and date', () => { + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.today instanceof Date).toBeTrue(); + const ss: any = TestBed.inject(SessionStorageService); + expect(ss.getItem).toHaveBeenCalledWith('benFlowID'); + }); + + it('fetches details and image for the route beneficiary', () => { + params.next({ beneficiaryRegID: '123' }); + expect(benService.getBeneficiaryDetails).toHaveBeenCalledWith('123', '77'); + expect(benService.getBeneficiaryImage).toHaveBeenCalledWith('123'); + expect(component.beneficiary).toBeUndefined(); + }); + + it('stores beneficiary, service date and image', () => { + details$.next({ + beneficiaryName: 'Ravi', + fatherName: 'Mohan', + lastName: 'K', + preferredPhoneNum: '999', + serviceDate: '2024-01-02', + }); + benService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'data:image/png;base64,xx' }) + ); + params.next({ beneficiaryRegID: '1' }); + fixture.detectChanges(); + expect(component.beneficiary.beneficiaryName).toBe('Ravi'); + expect(component.today).toBe('2024-01-02'); + expect(component.beneficiary.benImage).toBe('data:image/png;base64,xx'); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('img[alt="Patient"]')).not.toBeNull(); + expect(el.textContent).toContain('Mohan'); + expect(el.textContent).toContain('999'); + }); + + it('keeps today when no service date and default avatar when no image', () => { + details$.next({ beneficiaryName: 'A' }); + params.next({ beneficiaryRegID: '1' }); + fixture.detectChanges(); + expect(component.today instanceof Date).toBeTrue(); + expect( + fixture.nativeElement.querySelector('img[alt="DefaultPatient"]') + ).not.toBeNull(); + }); + + it('ngOnDestroy unsubscribes from the details stream', () => { + params.next({ beneficiaryRegID: '1' }); + const sub = component.beneficiaryDetailsSubscription; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('ngOnDestroy is safe without subscription', () => { + component.beneficiaryDetailsSubscription = undefined; + expect(() => component.ngOnDestroy()).not.toThrow(); }); }); diff --git a/src/app/app-modules/core/components/calibration/calibration.component.spec.ts b/src/app/app-modules/core/components/calibration/calibration.component.spec.ts index 4c88079b..991154d3 100644 --- a/src/app/app-modules/core/components/calibration/calibration.component.spec.ts +++ b/src/app/app-modules/core/components/calibration/calibration.component.spec.ts @@ -20,27 +20,164 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { MasterdataService } from 'src/app/app-modules/nurse-doctor/shared/services'; +import { ConfirmationService } from '../../services'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; import { CalibrationComponent } from './calibration.component'; describe('CalibrationComponent', () => { - let component: CalibrationComponent; let fixture: ComponentFixture; + let component: CalibrationComponent; + let master: any; + let confirm: any; + let ref: any; + const strips = [ + { sno: 1, stripCode: 'AB12', expiryDate: '2000-01-01' }, + { sno: 2, stripCode: 'CD34', expiryDate: '2999-01-01' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [CalibrationComponent], - }).compileComponents(); - })); - - beforeEach(() => { + const create = (response: any) => { + master.fetchCalibrationStrips.and.returnValue(response); fixture = TestBed.createComponent(CalibrationComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + beforeEach(async () => { + master = autoSpy(MasterdataService); + await TestBed.configureTestingModule({ + declarations: [CalibrationComponent], + providers: [ + ...commonTestProviders({ dialogData: { providerServiceMapID: 9 } }), + { provide: MasterdataService, useValue: master }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(CalibrationComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService); + ref = TestBed.inject(MatDialogRef); + }); + + afterEach(() => fixture.destroy()); + + it('loads strips for the provider on init', () => { + create(of({ statusCode: 200, data: { calibrationData: strips } })); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(master.fetchCalibrationStrips).toHaveBeenCalledWith(9, 0); + expect(component.components.data).toEqual(strips); + expect(component.dataList).toEqual(strips as any); + }); + + it('sets no-record message when list empty', () => { + create(of({ statusCode: 200, data: { calibrationData: [] } })); + expect(component.message).toBe(LANGUAGE_EN.common.noRecordFound); + expect(component.components.data).toEqual([]); + }); + + it('sets no-record message when data missing', () => { + create(of({ statusCode: 200, data: null })); + expect(component.message).toBe(LANGUAGE_EN.common.noRecordFound); + }); + + it('resets data on non-200 status', () => { + create(of({ statusCode: 5000 })); + expect(component.components.data).toEqual([]); + expect(component.message).toBe(''); + }); + + it('resets data on error', () => { + create(throwingObs()); + expect(component.components.data).toEqual([]); + }); + + describe('goToLink', () => { + beforeEach(() => + create(of({ statusCode: 200, data: { calibrationData: strips } })) + ); + + it('warns about expired strip and closes on confirm', () => { + component.goToLink(strips[0]); + expect(confirm.confirmCalibration).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.coreComponents.selectedCalibrationStripIs + ); + expect(ref.close).toHaveBeenCalledWith('AB12'); + }); + + it('does not close expired strip when declined', () => { + confirm.confirmCalibration.and.returnValue(of(false)); + component.goToLink(strips[0]); + expect(ref.close).not.toHaveBeenCalled(); + }); + + it('asks to proceed for valid strip and closes on confirm', () => { + component.goToLink(strips[1]); + expect(confirm.confirmCalibration).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.coreComponents + .doYouWantToProceedWithSelectedCalibrationStrip + ); + expect(ref.close).toHaveBeenCalledWith('CD34'); + }); + + it('treats a strip without expiry as valid; declined keeps dialog open', () => { + confirm.confirmCalibration.and.returnValue(of(false)); + component.goToLink({ stripCode: 'X' }); + expect(confirm.confirmCalibration).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.coreComponents + .doYouWantToProceedWithSelectedCalibrationStrip + ); + expect(ref.close).not.toHaveBeenCalled(); + }); + + it('close() closes with null', () => { + component.close(); + expect(ref.close).toHaveBeenCalledWith(null); + }); + }); + + describe('filterPreviousData', () => { + beforeEach(() => + create(of({ statusCode: 200, data: { calibrationData: strips } })) + ); + + it('restores the full list for an empty term', () => { + component.components.data = []; + component.filterPreviousData(''); + expect(component.components.data).toEqual(strips); + }); + + it('filters case-insensitively across all fields', () => { + component.filterPreviousData('cd3'); + expect(component.components.data).toEqual([strips[1]]); + }); + + it('matches each item only once', () => { + component.filterPreviousData('01-01'); + expect(component.components.data.length).toBe(2); + }); + + it('returns no items when nothing matches', () => { + component.filterPreviousData('zzz'); + expect(component.components.data).toEqual([]); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language', () => { + create(of({ statusCode: 200, data: { calibrationData: strips } })); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/core/components/camera-dialog/camera-dialog.component.spec.ts b/src/app/app-modules/core/components/camera-dialog/camera-dialog.component.spec.ts index d5f95b83..50cd450f 100644 --- a/src/app/app-modules/core/components/camera-dialog/camera-dialog.component.spec.ts +++ b/src/app/app-modules/core/components/camera-dialog/camera-dialog.component.spec.ts @@ -20,27 +20,273 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, +} from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ConfirmationService } from '../../services'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { CameraDialogComponent } from './camera-dialog.component'; +/** 1x1 transparent PNG. */ +const PNG = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + +const waitFor = async (cond: () => boolean, ms = 4000) => { + const start = Date.now(); + while (!cond() && Date.now() - start < ms) { + await new Promise(r => setTimeout(r, 20)); + } +}; + describe('CameraDialogComponent', () => { - let component: CameraDialogComponent; let fixture: ComponentFixture; + let component: CameraDialogComponent; + let confirm: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CameraDialogComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: 'B1', + visitID: 'V1', + userName: 'nurse', + providerServiceID: 7, + }, + }), + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CameraDialogComponent); component = fixture.componentInstance; + confirm = TestBed.inject(ConfirmationService); + }); + + afterEach(() => fixture.destroy()); + + it('sets default webcam options and capture status on init', () => { fixture.detectChanges(); + expect(component.options.width).toBe(500); + expect(component.options.video).toBeTrue(); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.status).toBe(LANGUAGE_EN.capture); + expect(component.loaded).toBeTrue(); + expect(fixture.nativeElement.textContent).toContain( + LANGUAGE_EN.coreComponents.noDataAvailableForThisBeneficiary + ); + }); + + describe('capture mode', () => { + beforeEach(() => { + component.capture = true; + fixture.detectChanges(); + }); + + it('renders the webcam placeholder (no real camera)', () => { + expect(fixture.nativeElement.querySelector('webcam')).not.toBeNull(); + }); + + it('getSnapshot and recaptureImage emit on the trigger observable', () => { + let count = 0; + const sub = component.triggerObservable.subscribe(() => count++); + component.getSnapshot(); + component.captured = true; + component.recaptureImage(); + expect(count).toBe(2); + expect(component.captured).toBeFalse(); + sub.unsubscribe(); + }); + + it('handleKeyDownRecaptureImg only reacts to Enter/Space', () => { + const spy = spyOn(component, 'recaptureImage'); + ['Enter', 'Spacebar', ' ', 'a'].forEach(key => + component.handleKeyDownRecaptureImg( + new KeyboardEvent('keydown', { key }) + ) + ); + expect(spy).toHaveBeenCalledTimes(3); + }); + + it('captureImg stores the image and shows preview', () => { + const img: any = { imageAsDataUrl: PNG }; + component.captureImg(img); + fixture.detectChanges(); + expect(component.webcamImage).toBe(img); + expect(component.sysImage).toBe(PNG); + expect(component.captured).toBeTrue(); + expect(fixture.nativeElement.querySelector('webcam')).toBeNull(); + expect( + fixture.nativeElement.querySelector('img[alt="sys"]') + ).not.toBeNull(); + }); + + it('captureImg with no image resets captured', () => { + component.captured = true; + component.captureImg(null as any); + expect(component.captured).toBeFalse(); + expect(component.status).toBe(LANGUAGE_EN.capture); + }); + + it('cancel button closes with false', () => { + const buttons = fixture.nativeElement.querySelectorAll('button'); + (buttons[0] as HTMLElement).click(); + expect((TestBed.inject(MatDialogRef) as any).close).toHaveBeenCalledWith( + false + ); + }); }); - it('should be created', () => { - expect(component).toBeTruthy(); + it('exposes nextWebcamObservable and no-op webcam callbacks', () => { + expect(component.nextWebcamObservable).toBeTruthy(); + expect(component.onSuccess({})).toBeUndefined(); + expect(component.onError({})).toBeUndefined(); + expect(component.handleInitError({} as any)).toBeUndefined(); + }); + + it('Confirm emits cancelEvent', () => { + const spy = jasmine.createSpy(); + component.cancelEvent.subscribe(spy); + component.Confirm(); + expect(spy).toHaveBeenCalledWith(null); + }); + + describe('annotate mode', () => { + it('loads existing markers onto the canvas and exposes them via getMarkers', () => { + component.annotate = PNG; + component.availablePoints = { + markers: [ + { xCord: 30, yCord: 40, description: 'Lesion', point: 1 }, + { xCord: 60, yCord: 80, point: 2 }, + ], + }; + // ngAfterViewInit mutates markers (bound to [disabled]) -> NG0100 in dev mode + expect(() => fixture.detectChanges()).toThrowError(/NG0100/); + expect(component.ctx).toBeTruthy(); + expect(component.markers).toEqual([ + { xCord: 30, yCord: 40, description: 'Lesion', point: 1 }, + { xCord: 60, yCord: 80, description: '', point: 1 }, + ]); + expect(component.score).toBe(2); + expect(component.getMarkers()).toEqual({ + beneficiaryRegID: 'B1', + visitID: 'V1', + createdBy: 'nurse', + imageID: '', + providerServiceMapID: 7, + markers: component.markers, + }); + }); + + it('draws marks at click offsets', () => { + component.annotate = PNG; + fixture.detectChanges(); + const stroke = spyOn(component.ctx, 'strokeRect'); + component.pointMark({ offsetX: 50, offsetY: 60 }); + expect(stroke).toHaveBeenCalledWith(40, 50, 20, 20); + expect(component.markers.length).toBe(1); + }); + + it('alerts instead of drawing beyond six markers', fakeAsync(() => { + component.annotate = PNG; + fixture.detectChanges(); + component.score = 7; + const stroke = spyOn(component.ctx, 'strokeRect'); + component.pointMark({ offsetX: 1, offsetY: 1 }); + flush(); + expect(stroke).not.toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.sixMakers + ); + })); + + it('clearPointers empties markers and redraws the image', async () => { + component.annotate = PNG; + fixture.detectChanges(); + component.pointMark({ offsetX: 10, offsetY: 10 }); + component.score = 4; + const clear = spyOn(component.ctx, 'clearRect').and.callThrough(); + component.clearPointers(); + expect(component.markers).toEqual([]); + expect(clear).toHaveBeenCalledWith(0, 0, 250, 250); + await waitFor(() => component.score === 1); + expect(component.score).toBe(1); + expect(component.ctx.font).toBe('bold 20px serif'); + }); + }); + + describe('downloadGraph', () => { + let errSpy: jasmine.Spy; + beforeEach(() => (errSpy = spyOn(console, 'error'))); + + it('logs when container is missing', () => { + fixture.detectChanges(); + component.downloadGraph(); + expect(errSpy).toHaveBeenCalledWith( + 'Element with ID "container-dialog" not found.' + ); + }); + + it('saves the rendered graph with a derived name', async () => { + component.graph = { type: 'bw' }; + fixture.detectChanges(); + const dispatch = spyOn( + HTMLAnchorElement.prototype, + 'dispatchEvent' + ).and.returnValue(true); + component.downloadGraph(); + await waitFor(() => dispatch.calls.count() > 0); + const anchor = dispatch.calls.mostRecent().object as HTMLAnchorElement; + expect(anchor.download).toBe('bw_B1_V1'); + }); + + it('falls back to a new window when saving fails', async () => { + component.graph = { type: 'bp' }; + fixture.detectChanges(); + const write = jasmine.createSpy('write'); + const open = spyOn(window, 'open').and.returnValue({ + document: { write }, + } as any); + component.graph = null; + component.downloadGraph(); + await waitFor(() => write.calls.count() > 0); + expect(open).toHaveBeenCalled(); + expect(write.calls.mostRecent().args[0]).toContain( + ' { - let component: DataSyncLoginComponent; let fixture: ComponentFixture; + let component: DataSyncLoginComponent; + let ds: any; + let confirm: any; + let session: any; + let router: Router; + let dialogRef: any; + let dialogData: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DataSyncLoginComponent], - }).compileComponents(); - })); + const mmuData = (extra: any = {}) => ({ + key: 'SERVER-KEY', + userID: 5, + previlegeObj: [ + { serviceName: 'MMU', providerServiceMapID: 42 }, + { serviceName: 'TM', providerServiceMapID: 1 }, + ], + ...extra, + }); - beforeEach(() => { + const create = (data: any = null) => { + dialogData = data; + TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: data }); + confirm = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture = TestBed.createComponent(DataSyncLoginComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + const fillForm = () => + component.loginForm.setValue({ + userName: 'syncuser', + password: 'Secret@1', + }); + + beforeEach(async () => { + ds = autoSpy(DataSyncService); + dialogRef = createDialogRefMock(); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [DataSyncLoginComponent], + providers: [ + ...commonTestProviders({ session: { userID: 5 } }), + { provide: MatDialogRef, useValue: dialogRef }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(DataSyncLoginComponent, { + set: { providers: [{ provide: DataSyncService, useValue: ds }] }, + }) + .compileComponents(); + }); + + afterEach(() => { + fixture?.destroy(); + sessionStorage.removeItem('serverKey'); + sessionStorage.removeItem('authorizeToViewTMcasesheet'); + }); + + it('initialises language, dialog ref and data', () => { + create({ masterDowloadFirstTime: true }); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.dialogRef).toBe(dialogRef); + expect(component.data).toEqual({ masterDowloadFirstTime: true }); + expect(component.loginForm.valid).toBeFalse(); + }); + + it('show/hide password toggles the input type', () => { + create(); + component.showPWD(); + fixture.detectChanges(); + const input = fixture.nativeElement.querySelector('#password'); + expect(input.type).toBe('text'); + component.hidePWD(); + fixture.detectChanges(); + expect(input.type).toBe('password'); + }); + + it('keySize and iterationCount accessors', () => { + create(); + expect(component.keySize).toBe(256); + expect(component.iterationCount).toBe(1989); + component.keySize = 128; + component.iterationCount = 10; + expect(component.keySize).toBe(128); + expect(component.iterationCount).toBe(10); + }); + + it('encrypt produces salt+iv+ciphertext that decrypts back to the plain text', () => { + create(); + component.iterationCount = 10; // keep test fast + const out = component.encrypt(component.Key_IV, 'hello'); + const salt = out.substring(0, 64); + const iv = out.substring(64, 96); + const cipher = out.substring(96); + const key = component.generateKey(salt, component.Key_IV); + const plain = CryptoJS.AES.decrypt( + { ciphertext: CryptoJS.enc.Base64.parse(cipher) } as any, + key, + { iv: CryptoJS.enc.Hex.parse(iv) } + ).toString(CryptoJS.enc.Utf8); + expect(plain).toBe('hello'); + }); + + describe('dataSyncLogin', () => { + beforeEach(() => { + create(); + spyOn(component, 'encrypt').and.returnValue('ENC'); + }); + + it('alerts when form is invalid', () => { + component.dataSyncLogin(); + expect(ds.dataSyncLogin).not.toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.usernamenPass + ); + expect(component.showProgressBar).toBeFalse(); + }); + + it('submitting the rendered form logs in with username and encrypted password', () => { + fillForm(); + ds.dataSyncLogin.and.returnValue( + of({ statusCode: 200, data: mmuData() }) + ); + fixture.debugElement + .query(By.css('form')) + .triggerEventHandler('submit', null); + expect(component.encrypt).toHaveBeenCalledWith( + 'Piramal12Piramal', + 'Secret@1' + ); + expect(ds.dataSyncLogin).toHaveBeenCalledWith('syncuser', 'ENC', false); + expect(session.setItem).toHaveBeenCalledWith('serverKey', 'SERVER-KEY'); + expect(session.setItem).toHaveBeenCalledWith( + 'dataSyncProviderServiceMapID', + 42 + ); + expect(sessionStorage.getItem('authorizeToViewTMcasesheet')).toBe( + 'NotAuthorized' + ); + expect(router.navigate).toHaveBeenCalledWith(['/datasync/workarea']); + expect(component.showProgressBar).toBeFalse(); + }); + + it('alerts when user has no MMU privilege', () => { + fillForm(); + sessionStorage.setItem('serverKey', 'old'); + ds.dataSyncLogin.and.returnValue( + of({ statusCode: 200, data: { previlegeObj: [{ serviceName: 'TM' }] } }) + ); + component.dataSyncLogin(); + expect(sessionStorage.getItem('serverKey')).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(NO_PRIV); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts when 200 has no data', () => { + fillForm(); + ds.dataSyncLogin.and.returnValue(of({ statusCode: 200, data: null })); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith(ELSEWHERE, 'error'); + }); + + it('alerts other 5002 errors', () => { + fillForm(); + ds.dataSyncLogin.and.returnValue( + of({ statusCode: 5002, errorMessage: 'Locked' }) + ); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith('Locked', 'error'); + expect(confirm.confirm).not.toHaveBeenCalled(); + }); + + it('alerts and marks not authorized on other status codes', () => { + fillForm(); + ds.dataSyncLogin.and.returnValue( + of({ statusCode: 5000, errorMessage: 'Bad creds' }) + ); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith('Bad creds', 'error'); + expect(sessionStorage.getItem('authorizeToViewTMcasesheet')).toBe( + 'NotAuthorized' + ); + expect(component.showProgressBar).toBeFalse(); + }); + + it('alerts on request error', () => { + fillForm(); + ds.dataSyncLogin.and.returnValue(throwingObs({ errorMessage: 'net' })); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith('net', 'error'); + expect(component.showProgressBar).toBeFalse(); + }); + + describe('already logged in elsewhere (5002)', () => { + const mmuRole = (serviceID: number) => ({ + roles: [ + { + serviceRoleScreenMappings: [ + { providerServiceMapping: { serviceID } }, + ], + }, + ], + serviceName: 'MMU', + providerServiceMapID: 42, + }); + + beforeEach(() => { + fillForm(); + ds.dataSyncLogin.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY }), + of({ + statusCode: 200, + data: { key: 'K2', userID: 5, previlegeObj: [mmuRole(2)] }, + }) + ); + ds.userlogoutPreviousSession.and.returnValue(of({ statusCode: 200 })); + }); + + it('logs out other session and logs in again when confirmed', () => { + component.dataSyncLogin(); + expect(confirm.confirm).toHaveBeenCalledWith('info', ALREADY); + expect(ds.userlogoutPreviousSession).toHaveBeenCalledWith('syncuser'); + expect(ds.dataSyncLogin.calls.mostRecent().args).toEqual([ + 'syncuser', + 'ENC', + true, + ]); + expect(session.setItem).toHaveBeenCalledWith('serverKey', 'K2'); + expect(router.navigate).toHaveBeenCalledWith(['/datasync/workarea']); + expect(component.showProgressBar).toBeFalse(); + }); + + it('stops when user declines', () => { + confirm.confirm.and.returnValue(of(false)); + component.dataSyncLogin(); + expect(ds.userlogoutPreviousSession).not.toHaveBeenCalled(); + expect(component.showProgressBar).toBeFalse(); + }); + + it('alerts when logout of previous session fails', () => { + ds.userlogoutPreviousSession.and.returnValue( + of({ statusCode: 5000, errorMessage: 'logout failed' }) + ); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith('logout failed', 'error'); + expect(ds.dataSyncLogin).toHaveBeenCalledTimes(1); + }); + + it('alerts when relogin fails', () => { + ds.dataSyncLogin.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY }), + of({ statusCode: 5000, errorMessage: 'relogin failed' }) + ); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith('relogin failed', 'error'); + expect(component.showProgressBar).toBeFalse(); + }); + + it('alerts when relogin has no data', () => { + ds.dataSyncLogin.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY }), + of({ statusCode: 200, data: null }) + ); + component.dataSyncLogin(); + expect(confirm.alert).toHaveBeenCalledWith(ELSEWHERE, 'error'); + }); + + it('alerts no privilege when relogin role is not service 2', () => { + sessionStorage.setItem('serverKey', 'old'); + ds.dataSyncLogin.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY }), + of({ + statusCode: 200, + data: { key: 'K', previlegeObj: [mmuRole(4)] }, + }) + ); + component.dataSyncLogin(); + expect(sessionStorage.getItem('serverKey')).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(NO_PRIV); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + }); + + describe('getDataSyncMMU', () => { + it('rejects a sync user different from the logged-in user', () => { + create(); + sessionStorage.setItem('serverKey', 'x'); + component.getDataSyncMMU({ data: mmuData({ userID: 99 }) }); + expect(sessionStorage.getItem('serverKey')).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Sync user is not valid. Please login with the correct credentials.', + 'error' + ); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('closes dialog with true on first-time master download', () => { + create({ masterDowloadFirstTime: true }); + component.getDataSyncMMU({ data: mmuData() }); + expect(dialogRef.close).toHaveBeenCalledWith(true); + expect(sessionStorage.getItem('authorizeToViewTMcasesheet')).toBeNull(); + }); + + it('authorizes TM casesheet viewing and closes dialog', () => { + create({ provideAuthorizationToViewTmCS: true }); + component.getDataSyncMMU({ + data: mmuData({ userID: undefined, previlegeObj: [] }), + }); + expect(sessionStorage.getItem('authorizeToViewTMcasesheet')).toBe( + 'Authorized' + ); + expect(session.setItem).not.toHaveBeenCalledWith( + 'dataSyncProviderServiceMapID', + jasmine.anything() + ); + expect(dialogRef.close).toHaveBeenCalledWith(true); + }); + + it('renders close icon only in dialog mode and closeDialog closes with false', () => { + create({ provideAuthorizationToViewTmCS: true }); + const closeEl = fixture.nativeElement.querySelector('.close-button'); + expect(closeEl).not.toBeNull(); + closeEl.click(); + expect(sessionStorage.getItem('authorizeToViewTMcasesheet')).toBe( + 'NotAuthorized' + ); + expect(dialogRef.close).toHaveBeenCalledWith(false); + }); + }); + + it('hides the close icon outside dialog mode', () => { + create(); + expect(fixture.nativeElement.querySelector('.close-button')).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language', () => { + create(); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(dialogData).toBeNull(); }); }); diff --git a/src/app/app-modules/core/components/iot-bluetooth/iot-bluetooth.component.spec.ts b/src/app/app-modules/core/components/iot-bluetooth/iot-bluetooth.component.spec.ts index 639faa9f..8f12dd29 100644 --- a/src/app/app-modules/core/components/iot-bluetooth/iot-bluetooth.component.spec.ts +++ b/src/app/app-modules/core/components/iot-bluetooth/iot-bluetooth.component.spec.ts @@ -20,27 +20,177 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { ConfirmationService } from '../../services'; +import { IotService } from '../../services/iot.service'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; import { IotBluetoothComponent } from './iot-bluetooth.component'; describe('IotBluetoothComponent', () => { - let component: IotBluetoothComponent; let fixture: ComponentFixture; + let component: IotBluetoothComponent; + let iot: any; + const connectedBody = { + deviceConnected: true, + bloodPressureIntro: true, + cholUaIntro: false, + glucometerIntro: true, + ecgIntro: false, + hbIntro: true, + pulseOxIntro: true, + urtCam1Intro: false, + bgbenecheck: true, + }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + iot = autoSpy(IotService); + iot.getDeviceStatus.and.returnValue( + of({ _body: JSON.stringify({ deviceConnected: false }) }) + ); + await TestBed.configureTestingModule({ declarations: [IotBluetoothComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders(), + { provide: IotService, useValue: iot }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(IotBluetoothComponent, '') + .compileComponents(); fixture = TestBed.createComponent(IotBluetoothComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('on init reports API available but device not connected', () => { + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.apiAvailable).toBeTrue(); + expect(component.deviceConnected).toBeFalse(); + expect(component.spinner).toBeFalse(); + expect(component.infoDetails.length).toBe(10); + }); + + it('getDeviceStatus configures modules when connected', () => { + iot.getDeviceStatus.and.returnValue( + of({ _body: JSON.stringify(connectedBody) }) + ); + component.errMsg = 'x'; + component.getDeviceStatus(); + expect(iot.setBluetoothConnected).toHaveBeenCalledWith(true); + expect(component.deviceConnected).toBeTrue(); + expect(component.errMsg).toBeUndefined(); + expect(component.infoDetails.slice(0, 8).map(i => i.status)).toEqual([ + true, + false, + true, + false, + true, + true, + false, + true, + ]); + }); + + it('getDeviceStatus error with non-object body means API available', () => { + iot.getDeviceStatus.and.returnValue(throwingObs({ _body: 'x' })); + component.getDeviceStatus(); + expect(component.apiAvailable).toBeTrue(); + expect(component.errMsg).toBeUndefined(); + expect(component.spinner).toBeFalse(); + }); + + it('getDeviceStatus error with object body means device not running', () => { + iot.getDeviceStatus.and.returnValue(throwingObs({ _body: {} })); + component.getDeviceStatus(); + expect(component.apiAvailable).toBeFalse(); + expect(component.errMsg).toBe('IOT Device is not running'); + }); + + it('getBluetoothDevice lists devices', () => { + iot.getBluetoothDevice.and.returnValue( + of({ _body: JSON.stringify(['dev1', 'dev2']) }) + ); + component.errMsg = 'x'; + component.getBluetoothDevice(); + expect(component.bluetoothDevices).toEqual(['dev1', 'dev2']); + expect(component.errMsg).toBeUndefined(); + expect(component.spinner).toBeFalse(); + }); + + it('getBluetoothDevice error stops spinner', () => { + iot.getBluetoothDevice.and.returnValue(throwingObs()); + component.getBluetoothDevice(); + expect(component.spinner).toBeFalse(); + expect(component.bluetoothDevices).toBeUndefined(); + }); + + it('connectBluetoothDevice configures the device', () => { + iot.connectBluetoothDevice.and.returnValue( + of({ _body: JSON.stringify(connectedBody) }) + ); + component.connectBluetoothDevice('dev1'); + expect(iot.connectBluetoothDevice).toHaveBeenCalledWith('dev1'); + expect(component.deviceConnected).toBeTrue(); + expect(component.spinner).toBeFalse(); + }); + + it('connectBluetoothDevice error stops spinner', () => { + iot.connectBluetoothDevice.and.returnValue(throwingObs()); + component.connectBluetoothDevice('d'); + expect(component.spinner).toBeFalse(); + expect(component.deviceConnected).toBeFalse(); + }); + + it('disconnectBluetoothDevice on success marks disconnected', () => { + component.deviceConnected = true; + iot.disconnectBluetoothDevice.and.returnValue(of({ status: 200 })); + component.disconnectBluetoothDevice(); + expect(iot.setBluetoothConnected).toHaveBeenCalledWith(false); + expect(component.deviceConnected).toBeFalse(); + expect(component.spinner).toBeFalse(); + }); + + it('disconnectBluetoothDevice on other status records message', () => { + iot.disconnectBluetoothDevice.and.returnValue( + of({ status: 500, message: 'busy' }) + ); + component.disconnectBluetoothDevice(); + expect(component.errMsg).toBe('busy'); + }); + + it('disconnectBluetoothDevice on error alerts', () => { + iot.disconnectBluetoothDevice.and.returnValue(throwingObs()); + component.disconnectBluetoothDevice(); + expect(component.spinner).toBeFalse(); + expect(TestBed.inject(ConfirmationService).alert).toHaveBeenCalledWith( + 'Unable to connect to bluetooth device', + 'error' + ); + }); + + it('pairDevice sets PC on success and R on failure', () => { + const temp: any = { pairAPI: '/pair', pairStatus: 'NP' }; + iot.pairExternalDevice.and.returnValue(of({})); + component.pairDevice(temp); + expect(iot.pairExternalDevice).toHaveBeenCalledWith('/pair'); + expect(temp.pairStatus).toBe('PC'); + iot.pairExternalDevice.and.returnValue(throwingObs()); + component.pairDevice(temp); + expect(temp.pairStatus).toBe('R'); + }); + + it('ngDoCheck refreshes the language', () => { + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/core/components/iotcomponent/iotcomponent.component.spec.ts b/src/app/app-modules/core/components/iotcomponent/iotcomponent.component.spec.ts index 617c183e..9e6889a1 100644 --- a/src/app/app-modules/core/components/iotcomponent/iotcomponent.component.spec.ts +++ b/src/app/app-modules/core/components/iotcomponent/iotcomponent.component.spec.ts @@ -20,27 +20,345 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogRef, +} from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { ConfirmationService } from '../../services'; +import { IotService } from '../../services/iot.service'; +import { CalibrationComponent } from '../calibration/calibration.component'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; import { IotcomponentComponent } from './iotcomponent.component'; describe('IotcomponentComponent', () => { - let component: IotcomponentComponent; let fixture: ComponentFixture; + let component: IotcomponentComponent; + let iot: any; + let ref: any; + let dialog: any; + let confirm: any; + let data: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [IotcomponentComponent], - }).compileComponents(); - })); + const calibProcedure = { + value: { + calibrationStartAPI: '/calib/start', + calibrationStatusAPI: '/calib/{cs_code}/status', + calibrationEndAPI: '/calib/end', + }, + }; - beforeEach(() => { + const create = () => { fixture = TestBed.createComponent(IotcomponentComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + beforeEach(async () => { + data = { startAPI: '/api/bp', output: undefined, procedure: undefined }; + iot = autoSpy(IotService, {}, { status: 500, message: 'nope' }); + await TestBed.configureTestingModule({ + declarations: [IotcomponentComponent], + providers: [ + ...commonTestProviders({ session: { providerServiceID: 4 } }), + { provide: MAT_DIALOG_DATA, useValue: data }, + { provide: IotService, useValue: iot }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(IotcomponentComponent, '') + .compileComponents(); + ref = TestBed.inject(MatDialogRef); + dialog = TestBed.inject(MatDialog); + confirm = TestBed.inject(ConfirmationService); + }); + + afterEach(() => fixture?.destroy()); + + describe('without calibration', () => { + it('starts the device API on init and sets language', () => { + create(); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(iot.startAPI).toHaveBeenCalledWith('/api/bp'); + expect(dialog.open).not.toHaveBeenCalled(); + expect(component.errorMsg).toBe('nope'); + }); + + it('polls status after 202 and closes with body when no output mapping', () => { + iot.startAPI.and.returnValue( + of({ status: 202, _body: { message: 'go' } }) + ); + iot.statusAPI.and.returnValue(of({ status: 200, _body: { sys: 120 } })); + create(); + expect(component.progressMsg).toBe('go'); + expect(iot.statusAPI).toHaveBeenCalledWith('/api/bp/status'); + expect(ref.close).toHaveBeenCalledWith({ sys: 120 }); + }); + + it('maps output keys when output list given', () => { + data.output = ['sys', 'dia']; + iot.startAPI.and.returnValue( + of({ status: 202, _body: { message: 'go' } }) + ); + iot.statusAPI.and.returnValue( + of({ status: 200, _body: { sys: 120, dia: 80, x: 1 } }) + ); + create(); + expect(ref.close).toHaveBeenCalledWith([120, 80]); + }); + + it('re-polls every 5s on 206 then stops on other status', fakeAsync(() => { + iot.startAPI.and.returnValue( + of({ status: 202, _body: { message: 'go' } }) + ); + iot.statusAPI.and.returnValues( + of({ status: 206, _body: { message: 'measuring' } }), + of({ status: 500, message: 'failed' }) + ); + create(); + expect(component.progressMsg).toBe('measuring'); + tick(5000); + expect(iot.statusAPI).toHaveBeenCalledTimes(2); + expect(component.errorMsg).toBe('failed'); + })); + + it('getstatus error with non-object body reads its message', () => { + create(); + iot.statusAPI.and.returnValue(throwingObs({ _body: 'text' })); + component.getstatus(); + expect(component.errorMsg).toBeUndefined(); + }); + + it('getstatus error with object body reports device not running', () => { + create(); + iot.statusAPI.and.returnValue(throwingObs({ _body: {} })); + component.getstatus(); + expect(component.errorMsg).toBe('Bluetooth Device is not running'); + }); + + it('start error with string body and message reads body message', () => { + create(); + iot.startAPI.and.returnValue(throwingObs({ _body: 'x', message: 'm' })); + component.errorMsg = 'old'; + component.start(); + expect(component.errorMsg).toBeUndefined(); + }); + + it('start error without message reports device not running', () => { + create(); + iot.startAPI.and.returnValue(throwingObs({ _body: 'x' })); + component.start(); + expect(component.errorMsg).toBe('Bluetooth Device is not running'); + }); + + it('start reports services not functional when service throws', () => { + create(); + iot.startAPI.and.throwError('down'); + component.start(); + expect(component.errorMsg).toBe('Services are not functional'); + }); + + it('stop without polling just closes', () => { + create(); + component.stop(); + expect(iot.endAPI).not.toHaveBeenCalled(); + expect(ref.close).toHaveBeenCalledWith(); + }); + + it('stop while polling ends the API and records errors', () => { + create(); + component.statuscall = 1; + component.stop(); + expect(iot.endAPI).toHaveBeenCalledWith('/api/bp'); + expect(component.errorMsg).toBe('nope'); + expect(ref.close).toHaveBeenCalled(); + }); + + it('stop while polling with 202 keeps error untouched', () => { + create(); + component.errorMsg = undefined; + component.statuscall = 1; + iot.endAPI.and.returnValue(of({ status: 202 })); + component.stop(); + expect(component.errorMsg).toBeUndefined(); + }); + }); + + describe('with calibration', () => { + beforeEach(() => { + data.procedure = calibProcedure; + }); + + const openWith = (result: any) => + dialog.open.and.returnValue(createDialogRefMock(result)); + + it('opens calibration dialog and starts calibration with chosen strip', () => { + openWith('STRIP1'); + iot.startAPI.and.returnValue( + of({ status: 202, _body: { message: 'insert strip' } }) + ); + create(); + expect(dialog.open).toHaveBeenCalledWith(CalibrationComponent, { + width: '600px', + disableClose: true, + data: { providerServiceMapID: 4 }, + }); + expect(component.stripCode).toBe('STRIP1'); + expect(component.msgCalibration).toBeTrue(); + expect(iot.startAPI).toHaveBeenCalledWith('/calib/start'); + expect(component.progressMsg).toBe('insert strip'); + expect(component.startedCalibration).toBeTrue(); + expect(component.stripShowMsg).toBeTrue(); + }); + + it('closes when calibration dialog returns null', () => { + openWith(null); + create(); + expect(ref.close).toHaveBeenCalled(); + expect(iot.startAPI).not.toHaveBeenCalled(); + }); + + it('does nothing when calibration dialog is dismissed with undefined', () => { + openWith(undefined); + create(); + expect(component.stripCode).toBeUndefined(); + expect(ref.close).not.toHaveBeenCalled(); + }); + + it('calibStart records non-202 message', () => { + openWith('S'); + create(); + expect(component.errorMsg).toBe('nope'); + }); + + it('calibStart error handling', () => { + openWith(undefined); + create(); + iot.startAPI.and.returnValue(throwingObs({ _body: {} })); + component.calibStart(); + expect(component.errorMsg).toBe('Bluetooth Device is not running'); + iot.startAPI.and.returnValue(throwingObs({ _body: 'x' })); + component.calibStart(); + expect(component.errorMsg).toBeUndefined(); + iot.startAPI.and.throwError('x'); + component.calibStart(); + expect(component.errorMsg).toBe('Services are not functional'); + }); + + describe('getCalibStatus', () => { + beforeEach(() => { + openWith(undefined); + create(); + component.stripCode = 'S9'; + }); + + it('on success asks to continue and starts measurement when confirmed', () => { + iot.statusAPI.and.returnValue( + of({ status: 200, _body: { message: 'ok' } }) + ); + const startSpy = spyOn(component, 'start'); + component.getCalibStatus(); + expect(iot.statusAPI).toHaveBeenCalledWith('/calib/S9/status'); + expect(component.statusCalibration).toBeTrue(); + expect(component.progressMsg).toBe('ok'); + expect(confirm.confirmCalibration).toHaveBeenCalledWith( + 'success', + LANGUAGE_EN.calibrationTestSuccess + ); + expect(startSpy).toHaveBeenCalled(); + }); + + it('on 202 closes dialog when not confirmed', () => { + iot.statusAPI.and.returnValue( + of({ status: 202, _body: { message: 'ok' } }) + ); + confirm.confirmCalibration.and.returnValue(of(false)); + component.getCalibStatus(); + expect(ref.close).toHaveBeenCalled(); + }); + + it('on 206 polls again after 5s', fakeAsync(() => { + iot.statusAPI.and.returnValues( + of({ status: 206, _body: { message: 'wait' } }), + of({ status: 400, message: 'bad strip' }) + ); + component.getCalibStatus(); + expect(component.progressMsg).toBe('wait'); + tick(5000); + expect(component.errorMsg).toBe('bad strip'); + expect(component.stripShowMsg).toBeFalse(); + })); + + it('handles errors', () => { + iot.statusAPI.and.returnValue(throwingObs({ _body: {} })); + component.getCalibStatus(); + expect(component.errorMsg).toBe('Bluetooth Device is not running'); + iot.statusAPI.and.returnValue(throwingObs({ _body: 'x' })); + component.getCalibStatus(); + expect(component.errorMsg).toBeUndefined(); + expect(component.statusCalibration).toBeTrue(); + }); + }); + + describe('calibStop', () => { + beforeEach(() => { + openWith(undefined); + create(); + component.msgCalibration = true; + }); + + [undefined, 5].forEach(statuscall => { + describe(`with statuscall=${statuscall}`, () => { + beforeEach(() => (component.statuscall = statuscall)); + + it('marks stopped on 200', () => { + iot.endCalibrationAPI.and.returnValue(of({ status: 200 })); + component.stop(); + expect(iot.endCalibrationAPI).toHaveBeenCalledWith('/calib/end'); + expect(component.stoppedCalibration).toBeTrue(); + expect(ref.close).toHaveBeenCalled(); + }); + + it('closes with error message on failure status', () => { + iot.endCalibrationAPI.and.returnValue( + of({ status: 500, message: 'e' }) + ); + component.calibStop(); + expect(component.errorMsg).toBe('e'); + expect(ref.close).toHaveBeenCalledWith('e'); + }); + + it('handles errors', () => { + iot.endCalibrationAPI.and.returnValue(throwingObs({ _body: {} })); + component.calibStop(); + expect(component.errorMsg).toBe('Bluetooth Device is not running'); + iot.endCalibrationAPI.and.returnValue(throwingObs({ _body: 'x' })); + component.calibStop(); + expect(component.errorMsg).toBeUndefined(); + }); + }); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language', () => { + create(); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/core/components/open-previous-visit-details/open-previous-visit-details.component.spec.ts b/src/app/app-modules/core/components/open-previous-visit-details/open-previous-visit-details.component.spec.ts index 3af57fb7..b4efffee 100644 --- a/src/app/app-modules/core/components/open-previous-visit-details/open-previous-visit-details.component.spec.ts +++ b/src/app/app-modules/core/components/open-previous-visit-details/open-previous-visit-details.component.spec.ts @@ -20,27 +20,149 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { DoctorService } from 'src/app/app-modules/nurse-doctor/shared/services'; +import { ConfirmationService } from '../../services/confirmation.service'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; import { OpenPreviousVisitDetailsComponent } from './open-previous-visit-details.component'; describe('OpenPreviousVisitDetailsComponent', () => { - let component: OpenPreviousVisitDetailsComponent; let fixture: ComponentFixture; + let component: OpenPreviousVisitDetailsComponent; + let doctor: any; + let confirm: any; + const visits = [ + { + VisitCategory: 'General OPD', + benFlowID: 1, + beneficiaryRegID: 11, + visitCode: 101, + }, + { + VisitCategory: 'ANC', + benFlowID: 2, + beneficiaryRegID: 11, + visitCode: null, + }, + { + VisitCategory: 'NCD care', + benFlowID: 3, + beneficiaryRegID: 11, + visitCode: 103, + }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [OpenPreviousVisitDetailsComponent], - }).compileComponents(); - })); - - beforeEach(() => { + const create = (history: any) => { + doctor.getMMUHistory.and.returnValue(history); fixture = TestBed.createComponent(OpenPreviousVisitDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + declarations: [OpenPreviousVisitDetailsComponent], + providers: [ + ...commonTestProviders(), + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(OpenPreviousVisitDetailsComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('loads history and fetches casesheet for visits with a visit code', () => { + doctor.getMMUCasesheetData.and.callFake((req: any) => + of({ statusCode: 200, data: { code: req.visitCode } }) + ); + create(of({ statusCode: 200, data: visits.map(v => ({ ...v })) })); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledTimes(2); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'General OPD', + benFlowID: 1, + beneficiaryRegID: 11, + visitCode: 101, + }); + expect(component.previousVisitData[0].benPreviousData).toEqual({ + code: 101, + }); + expect(component.previousVisitData[1].benPreviousData).toBeUndefined(); + expect(component.filteredHistory).toEqual({ code: 103 }); + expect(component.previousHistoryPagedList.length).toBe(3); + }); + + it('ignores casesheet responses that are not 200 or have null data', () => { + doctor.getMMUCasesheetData.and.returnValues( + of({ statusCode: 500, data: {} }), + of({ statusCode: 200, data: null }) + ); + create(of({ statusCode: 200, data: visits.map(v => ({ ...v })) })); + expect(component.previousVisitData[0].benPreviousData).toBeUndefined(); + expect(component.previousVisitData[2].benPreviousData).toBeUndefined(); + expect(component.filteredHistory).toEqual([]); + }); + + it('alerts when history status is not 200', () => { + create(of({ statusCode: 5000 })); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.unableToLoadData, + 'error' + ); + expect(component.previousVisitData).toEqual([]); + }); + + it('alerts when history request errors', () => { + create(throwingObs()); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.unableToLoadData, + 'error' + ); + }); + + it('pages at most 5 visits', () => { + const many = Array.from({ length: 7 }, (_, i) => ({ + VisitCategory: 'V' + i, + })); + create(of({ statusCode: 200, data: many })); + expect(component.previousHistoryPagedList).toEqual(many.slice(0, 5)); + }); + + describe('filterHistory', () => { + beforeEach(() => + create(of({ statusCode: 200, data: visits.map(v => ({ ...v })) })) + ); + + it('returns all visits for empty term and resets the page', () => { + component.previousHistoryActivePage = 3; + component.filterHistory(); + expect(component.filteredHistory).toBe(component.previousVisitData); + expect(component.previousHistoryActivePage).toBe(1); + }); + + it('filters on visit category case-insensitively', () => { + component.filterHistory('ncd'); + expect( + component.filteredHistory.map((v: any) => v.VisitCategory) + ).toEqual(['NCD care']); + }); + + it('re-appends visits to the paged list on every call (current behaviour)', () => { + const before = component.previousHistoryPagedList.length; + component.filterHistory('anc'); + expect(component.previousHistoryPagedList.length).toBe(before + 3); + }); }); }); diff --git a/src/app/app-modules/core/components/previous-details/previous-details.component.spec.ts b/src/app/app-modules/core/components/previous-details/previous-details.component.spec.ts index 4e75fa0e..29279a01 100644 --- a/src/app/app-modules/core/components/previous-details/previous-details.component.spec.ts +++ b/src/app/app-modules/core/components/previous-details/previous-details.component.spec.ts @@ -20,27 +20,116 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { PreviousDetailsComponent } from './previous-details.component'; describe('PreviousDetailsComponent', () => { - let component: PreviousDetailsComponent; let fixture: ComponentFixture; + let component: PreviousDetailsComponent; + const rows = [ + { name: 'Paracetamol', dose: '500mg' }, + { name: 'Ibuprofen', dose: '200mg' }, + ]; + const columns = [ + { columnName: 'Name', keyName: 'name' }, + { columnName: 'Dose', keyName: 'dose' }, + { columnName: 'NoKey' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const create = async (dataList: any) => { + await TestBed.configureTestingModule({ declarations: [PreviousDetailsComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [...commonTestProviders({ dialogData: { dataList } })], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PreviousDetailsComponent, '') + .compileComponents(); fixture = TestBed.createComponent(PreviousDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + afterEach(() => fixture.destroy()); + + it('loads data and builds displayed columns from keyed columns', async () => { + await create({ data: rows, columns }); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.dataList).toBe(rows); + expect(component.filteredDataList.data).toEqual(rows); + expect(component.filteredDataList.data).not.toBe(rows); + expect(component.columnList).toBe(columns); + expect(component.displayedColumns).toEqual(['sno', 'name', 'dose']); + }); + + it('ignores non-array data', async () => { + await create({ data: 'oops', columns: [] }); + expect(component.dataList).toEqual([]); + expect(component.filteredDataList.data).toEqual([]); + expect(component.displayedColumns).toEqual(['sno']); + }); + + it('ignores null data', async () => { + await create({ data: null, columns: [] }); + expect(component.dataList).toEqual([]); + }); + + it('throws when columns are missing (unguarded filter call)', async () => { + await TestBed.configureTestingModule({ + declarations: [PreviousDetailsComponent], + providers: [ + ...commonTestProviders({ dialogData: { dataList: { data: rows } } }), + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PreviousDetailsComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(PreviousDetailsComponent); + component = fixture.componentInstance; + expect(() => component.ngOnInit()).toThrowError(TypeError); + expect(component.columnList).toEqual([]); + }); + + describe('filterPreviousData', () => { + beforeEach(async () => create({ data: rows, columns })); + + it('restores all rows for empty term', () => { + component.filteredDataList.data = []; + component.filterPreviousData(''); + expect(component.filteredDataList.data).toBe(rows); + }); + + it('filters case-insensitively on any field', () => { + component.filterPreviousData('IBU'); + expect(component.filteredDataList.data).toEqual([rows[1]]); + }); + + it('adds each matching row once', () => { + component.filterPreviousData('mg'); + expect(component.filteredDataList.data).toEqual(rows); + }); + + it('returns empty when no match', () => { + component.filterPreviousData('zzz'); + expect(component.filteredDataList.data).toEqual([]); + }); + }); + + it('closeDialog closes the dialog', async () => { + await create({ data: rows, columns }); + component.closeDialog(); + expect((TestBed.inject(MatDialogRef) as any).close).toHaveBeenCalled(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language', async () => { + await create({ data: rows, columns }); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/core/components/provisional-search/provisional-search.component.spec.ts b/src/app/app-modules/core/components/provisional-search/provisional-search.component.spec.ts index 342418c2..1ec917d3 100644 --- a/src/app/app-modules/core/components/provisional-search/provisional-search.component.spec.ts +++ b/src/app/app-modules/core/components/provisional-search/provisional-search.component.spec.ts @@ -20,27 +20,152 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { MasterdataService } from '../../../nurse-doctor/shared/services/masterdata.service'; +import { SpinnerService } from '../../services/spinner.service'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; import { ProvisionalSearchComponent } from './provisional-search.component'; describe('ProvisionalSearchComponent', () => { - let component: ProvisionalSearchComponent; let fixture: ComponentFixture; + let component: ProvisionalSearchComponent; + let master: any; + const rows = [ + { conceptID: 'C1', term: 'Fever' }, + { conceptID: 'C2', term: 'Cough' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const create = async ( + input: any, + response: any = of({ statusCode: 200 }) + ) => { + master = autoSpy(MasterdataService); + master.searchDiagnosisBasedOnPageNo.and.returnValue(response); + await TestBed.configureTestingModule({ declarations: [ProvisionalSearchComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ dialogData: input }), + { provide: MasterdataService, useValue: master }, + { provide: SpinnerService, useValue: autoSpy(SpinnerService) }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(ProvisionalSearchComponent, '') + .compileComponents(); fixture = TestBed.createComponent(ProvisionalSearchComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + afterEach(() => fixture.destroy()); + + it('searches on init and sets placeholder from diagnosis type', async () => { + await create( + { searchTerm: 'fev', diagonasisType: 'Provisional', addedDiagnosis: [] }, + of({ statusCode: 200, data: { sctMaster: rows } }) + ); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith('fev', 0); + expect(component.diagnosis.data).toEqual(rows); + expect(component.placeHolderSearch).toBe('Provisional'); + expect(component.showProgressBar).toBeFalse(); + }); + + it('does not search short terms nor set placeholder without type', async () => { + await create({ searchTerm: 'fe', addedDiagnosis: [] }); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + expect(component.placeHolderSearch).toBeUndefined(); + }); + + it('leaves table empty when result has no rows', async () => { + await create( + { searchTerm: 'xyz', addedDiagnosis: [] }, + of({ statusCode: 200, data: { sctMaster: [] } }) + ); + expect(component.diagnosis.data).toEqual([]); + expect(component.showProgressBar).toBeFalse(); + }); + + it('resets on non-200', async () => { + await create( + { searchTerm: 'xyz', addedDiagnosis: [] }, + of({ statusCode: 5000 }) + ); + expect(component.diagnosis.data).toEqual([]); + expect(component.showProgressBar).toBeFalse(); + }); + + it('resets on error', async () => { + await create({ searchTerm: 'xyz', addedDiagnosis: [] }, throwingObs()); + expect(component.diagnosis.data).toEqual([]); + expect(component.showProgressBar).toBeFalse(); + }); + + describe('selection', () => { + beforeEach(async () => + create({ searchTerm: 'ab', addedDiagnosis: [{ conceptID: 'A0' }] }) + ); + + it('selectDiagnosis adds and removes items', () => { + const a: any = { conceptID: 'C1' }; + const b: any = { conceptID: 'C2' }; + component.selectDiagnosis({ checked: true }, a); + component.selectDiagnosis({ checked: true }, b); + expect(a.selected).toBeTrue(); + expect(component.selectedDiagnosisList).toEqual([a, b]); + component.selectDiagnosis({ checked: false }, a); + expect(a.selected).toBeFalse(); + expect(component.selectedDiagnosisList).toEqual([b]); + }); + + it('selectedDiagnosis is true for already-added or currently selected', () => { + expect(component.selectedDiagnosis({ conceptID: 'A0' })).toBeTrue(); + component.selectedDiagnosisList = [{ conceptID: 'C1' }]; + expect(component.selectedDiagnosis({ conceptID: 'C1' })).toBeTrue(); + expect(component.selectedDiagnosis({ conceptID: 'C9' })).toBeFalse(); + }); + + it('disableSelection disables already-added items', () => { + expect(component.disableSelection({ conceptID: 'A0' })).toBeTrue(); + }); + + it('disableSelection keeps selected items enabled', () => { + component.selectedDiagnosisList = [{ conceptID: 'C1' }]; + expect(component.disableSelection({ conceptID: 'C1' })).toBeFalse(); + }); + + it('disableSelection allows new items under the 30 limit', () => { + expect(component.disableSelection({ conceptID: 'C5' })).toBeFalse(); + }); + + it('disableSelection blocks new items once 30 are chosen', () => { + component.selectedDiagnosisList = Array.from({ length: 30 }, (_, i) => ({ + conceptID: 'S' + i, + })); + expect(component.disableSelection({ conceptID: 'C5' })).toBeTrue(); + }); + + it('submitDiagnosisList closes with selected list', () => { + component.selectedDiagnosisList = [{ conceptID: 'C1' }]; + component.submitDiagnosisList(); + expect((TestBed.inject(MatDialogRef) as any).close).toHaveBeenCalledWith([ + { conceptID: 'C1' }, + ]); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language', async () => { + await create({ searchTerm: '', addedDiagnosis: [] }); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toBe(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/core/components/set-language.component.spec.ts b/src/app/app-modules/core/components/set-language.component.spec.ts new file mode 100644 index 00000000..2846107b --- /dev/null +++ b/src/app/app-modules/core/components/set-language.component.spec.ts @@ -0,0 +1,72 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { BehaviorSubject, Subject } from 'rxjs'; +import { SetLanguageComponent } from './set-language.component'; +import { LANGUAGE_EN } from 'src/testing/test-utils'; + +describe('SetLanguageComponent', () => { + it('copies the current language object from the service', () => { + const svc: any = { + currentLangugae$: new BehaviorSubject(LANGUAGE_EN).asObservable(), + }; + const c = new SetLanguageComponent(svc); + expect(c.currentLanguageObject).toBeUndefined(); + c.setLanguage(); + expect(c.currentLanguageObject).toBe(LANGUAGE_EN); + }); + + it('unsubscribes immediately so later emissions are ignored', () => { + const subj = new BehaviorSubject({ a: 1 }); + const c = new SetLanguageComponent({ + currentLangugae$: subj.asObservable(), + } as any); + c.setLanguage(); + subj.next({ a: 2 }); + expect(c.currentLanguageObject).toEqual({ a: 1 }); + expect(subj.observers.length).toBe(0); + }); + + it('leaves object undefined when nothing has been emitted', () => { + const c = new SetLanguageComponent({ + currentLangugae$: new Subject().asObservable(), + } as any); + c.setLanguage(); + expect(c.currentLanguageObject).toBeUndefined(); + }); + + it('logs errors emitted by the language stream', () => { + const spy = spyOn(console, 'error'); + const c = new SetLanguageComponent({ + currentLangugae$: { + subscribe: (n: any, e: any, done: any) => { + e('bad'); + done(); + return { unsubscribe: () => undefined }; + }, + }, + } as any); + c.setLanguage(); + expect(spy).toHaveBeenCalledWith('bad'); + expect(c.currentLanguageObject).toBeUndefined(); + }); +}); diff --git a/src/app/app-modules/core/components/show-commit-and-version-details/show-commit-and-version-details.component.spec.ts b/src/app/app-modules/core/components/show-commit-and-version-details/show-commit-and-version-details.component.spec.ts index 99c0d817..c424575b 100644 --- a/src/app/app-modules/core/components/show-commit-and-version-details/show-commit-and-version-details.component.spec.ts +++ b/src/app/app-modules/core/components/show-commit-and-version-details/show-commit-and-version-details.component.spec.ts @@ -20,27 +20,56 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpServiceService } from '../../services/http-service.service'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { ShowCommitAndVersionDetailsComponent } from './show-commit-and-version-details.component'; describe('ShowCommitAndVersionDetailsComponent', () => { - let component: ShowCommitAndVersionDetailsComponent; let fixture: ComponentFixture; + let component: ShowCommitAndVersionDetailsComponent; + const input = { + commitDetailsAPI: { version: '3.1.0', commit: 'abc123' }, + commitDetailsUI: { version: '3.2.0', commit: 'def456' }, + }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [ShowCommitAndVersionDetailsComponent], + providers: [...commonTestProviders({ dialogData: input })], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ShowCommitAndVersionDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('assigns the language set on init', () => { + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.input).toBe(input); + }); + + it('renders API and UI version and commit details', () => { + const cells = Array.from( + fixture.nativeElement.querySelectorAll('td') as NodeListOf + ).map(td => td.textContent?.trim()); + expect(cells).toEqual(['3.1.0', '3.2.0', 'abc123', 'def456']); + }); + + it('ngDoCheck re-reads the current language', () => { + const http: any = TestBed.inject(HttpServiceService); + const lang = { common: { versionDetails: 'VD' } }; + http.appCurrentLanguge.next(lang); + fixture.detectChanges(); + expect(component.current_language_set).toBe(lang); + expect(fixture.nativeElement.querySelector('h4').textContent).toContain( + 'VD' + ); }); }); diff --git a/src/app/app-modules/core/components/spinner/spinner.component.spec.ts b/src/app/app-modules/core/components/spinner/spinner.component.spec.ts index dad922e9..8bd8ff36 100644 --- a/src/app/app-modules/core/components/spinner/spinner.component.spec.ts +++ b/src/app/app-modules/core/components/spinner/spinner.component.spec.ts @@ -21,24 +21,41 @@ */ import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { SpinnerService } from '../../services'; +import { NO_ERRORS_SCHEMA } from 'src/testing/test-utils'; import { SpinnerComponent } from './spinner.component'; describe('SpinnerComponent', () => { - let component: SpinnerComponent; let fixture: ComponentFixture; + let service: SpinnerService; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [SpinnerComponent], + providers: [SpinnerService], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(SpinnerComponent); - component = fixture.componentInstance; + service = TestBed.inject(SpinnerService); + }); + + afterEach(() => fixture.destroy()); + + it('exposes the injected spinner service', () => { + expect(fixture.componentInstance.spinnerService).toBe(service); + }); + + it('hides the overlay when not loading', () => { + service.setLoading(false); fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('.overlay')).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('shows the overlay with spinner while loading', () => { + service.setLoading(true); + fixture.detectChanges(); + const overlay = fixture.nativeElement.querySelector('.overlay'); + expect(overlay).not.toBeNull(); + expect(overlay.querySelector('mat-spinner')).not.toBeNull(); }); }); diff --git a/src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component.spec.ts b/src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component.spec.ts index 3c9a5884..7a0554f6 100644 --- a/src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component.spec.ts +++ b/src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component.spec.ts @@ -20,27 +20,71 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { ViewRadiologyUploadedFilesComponent } from './view-radiology-uploaded-files.component'; describe('ViewRadiologyUploadedFilesComponent', () => { - let component: ViewRadiologyUploadedFilesComponent; let fixture: ComponentFixture; + let component: ViewRadiologyUploadedFilesComponent; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const create = async (data: any) => { + await TestBed.configureTestingModule({ declarations: [ViewRadiologyUploadedFilesComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideProvider(MAT_DIALOG_DATA, { useValue: data }) + .compileComponents(); fixture = TestBed.createComponent(ViewRadiologyUploadedFilesComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + afterEach(() => fixture.destroy()); + + it('lists the files passed in and closes with the clicked file', async () => { + const files = [ + { fileName: 'xray.png', id: 1 }, + { fileName: 'scan.pdf', id: 2 }, + ]; + await create({ filesDetails: files }); + expect(component.current_language_set).toBe(LANGUAGE_EN); + expect(component.fileIds).toEqual(files as any); + const rows = fixture.nativeElement.querySelectorAll('tr.list-style'); + expect(rows.length).toBe(2); + expect(rows[1].textContent).toContain('scan.pdf'); + (rows[1].querySelectorAll('td')[1] as HTMLElement).click(); + const ref: any = TestBed.inject(MatDialogRef); + expect(ref.close).toHaveBeenCalledWith(files[1]); + }); + + it('shows no-records message when first file id is null', async () => { + await create({ filesDetails: [null] }); + expect(fixture.nativeElement.querySelector('tr.list-style')).toBeNull(); + expect(fixture.nativeElement.textContent).toContain( + LANGUAGE_EN.noRecordsFound + ); + }); + + it('keeps empty file list when no filesDetails are provided', async () => { + await create({}); + expect(component.fileIds).toEqual([]); + }); + + it('tolerates null dialog input', async () => { + await create(null); + expect(component.fileIds).toEqual([]); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('getFilesDetails assigns directly', async () => { + await create({}); + component.getFilesDetails([{ fileName: 'a' }]); + expect(component.fileIds).toEqual([{ fileName: 'a' }] as any); }); }); diff --git a/src/app/app-modules/core/directives/MobileNumber/myMobileNumber.directive.spec.ts b/src/app/app-modules/core/directives/MobileNumber/myMobileNumber.directive.spec.ts new file mode 100644 index 00000000..7df7b742 --- /dev/null +++ b/src/app/app-modules/core/directives/MobileNumber/myMobileNumber.directive.spec.ts @@ -0,0 +1,140 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MyMobileNumberDirective } from './myMobileNumber.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent {} + +describe('MyMobileNumberDirective', () => { + let fixture: ComponentFixture; + let inputEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MyMobileNumberDirective, TestHostComponent], + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + inputEl = fixture.debugElement.query(By.directive(MyMobileNumberDirective)); + }); + + it('should create the directive on the host element', () => { + expect(inputEl).toBeTruthy(); + }); + + describe('keypress - input filtering', () => { + function keypress(charCode: number, which: number) { + return { + charCode, + which, + preventDefault: jasmine.createSpy('preventDefault'), + }; + } + + it('does not currently block alphabetic characters (malformed regex never matches a single char)', () => { + const event = keypress(65, 65); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('does not currently block lowercase alphabetic characters (malformed regex never matches a single char)', () => { + const event = keypress(97, 97); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('does not currently block special characters like @ (malformed regex never matches a single char)', () => { + const event = keypress(64, 64); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow digit characters', () => { + const event = keypress(48, 48); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow digit 9', () => { + const event = keypress(57, 57); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow space character', () => { + const event = keypress(32, 32); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should prevent default when the key matches the filter regex', () => { + const test = jasmine.createSpy('test').and.returnValue(true); + // jasmine restores window.RegExp after this spec + spyOn(window as any, 'RegExp').and.returnValue({ test }); + const event = keypress(65, 65); + inputEl.triggerEventHandler('keypress', event); + expect(test).toHaveBeenCalledWith(String.fromCharCode(65)); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should use ev.which when charCode is 0', () => { + const test = jasmine.createSpy('test').and.returnValue(true); + spyOn(window as any, 'RegExp').and.returnValue({ test }); + const event = keypress(0, 65); + inputEl.triggerEventHandler('keypress', event); + expect(test).toHaveBeenCalledWith(String.fromCharCode(65)); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/confirmatory-diagnosis.directive.spec.ts b/src/app/app-modules/core/directives/confirmatory-diagnosis.directive.spec.ts new file mode 100644 index 00000000..1d28b620 --- /dev/null +++ b/src/app/app-modules/core/directives/confirmatory-diagnosis.directive.spec.ts @@ -0,0 +1,323 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FormBuilder, + FormControl, + FormGroup, + FormArray, + ReactiveFormsModule, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { of, BehaviorSubject } from 'rxjs'; +import { ConfirmatoryDiagnosisDirective } from './confirmatory-diagnosis.directive'; +import { HttpServiceService } from '../services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +@Component({ + template: ` +
+
+
+ + +
+
+
+ `, +}) +class TestHostComponent { + previousSelected: any[] = []; + parentForm: FormGroup; + diagnosisGroup: FormGroup; + + constructor(private fb: FormBuilder) { + this.parentForm = this.fb.group({ + diagnosisList: this.fb.array([ + this.fb.group({ + confirmatoryDiagnosis: [''], + term: [''], + conceptID: [''], + }), + ]), + }); + const diagArray = this.parentForm.get('diagnosisList') as FormArray; + this.diagnosisGroup = diagArray.at(0) as FormGroup; + } +} + +describe('ConfirmatoryDiagnosisDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let mockDialog: jasmine.SpyObj; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + let languageSubject: BehaviorSubject; + + beforeEach(() => { + languageSubject = new BehaviorSubject({ + confirmDiagnosis: 'Confirmatory Diagnosis', + }); + + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockHttpService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: languageSubject.asObservable(), + }); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + mockSessionStorage.getItem.and.returnValue(null); + + TestBed.configureTestingModule({ + declarations: [ConfirmatoryDiagnosisDirective, TestHostComponent], + imports: [ReactiveFormsModule], + providers: [ + FormBuilder, + { provide: MatDialog, useValue: mockDialog }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create directives on host elements', () => { + const directives = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + ); + expect(directives.length).toBe(2); + }); + + it('should fetch language response on init', () => { + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + const directive = buttonEl.injector.get(ConfirmatoryDiagnosisDirective); + expect(directive.currentLanguageSet).toBeDefined(); + expect(directive.currentLanguageSet.confirmDiagnosis).toBe( + 'Confirmatory Diagnosis' + ); + }); + + describe('click handler', () => { + it('should open dialog when non-INPUT element is clicked', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(mockDialog.open).toHaveBeenCalled(); + }); + + it('should NOT open dialog when INPUT element is clicked', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + + const inputEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[0]; + inputEl.triggerEventHandler('click', null); + + expect(mockDialog.open).not.toHaveBeenCalled(); + }); + }); + + describe('keyup.enter handler', () => { + it('should open dialog on Enter key', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('keyup.enter', null); + + expect(mockDialog.open).toHaveBeenCalled(); + }); + }); + + describe('openDialog', () => { + it('should not open dialog when search term is 2 chars or less', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('ab'); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(mockDialog.open).not.toHaveBeenCalled(); + }); + + it('should pass correct data to dialog', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + component.previousSelected = [{ term: 'existing' }]; + fixture.detectChanges(); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + const callArgs: any[] = mockDialog.open.calls.mostRecent().args; + expect(callArgs[1]?.data.searchTerm).toBe('Diabetes mellitus'); + expect(callArgs[1]?.data.addedDiagnosis).toEqual([{ term: 'existing' }]); + expect(callArgs[1]?.data.diagonasisType).toBe('Confirmatory Diagnosis'); + expect(callArgs[1]?.width).toBe('800px'); + }); + + it('should handle dialog result with diagnosis data', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + + const result = [{ term: 'Type 2 Diabetes', conceptID: 'C001' }]; + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(result)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(diagGroup.controls['term'].value).toBe('Type 2 Diabetes'); + expect(diagGroup.controls['conceptID'].value).toBe('C001'); + expect(diagGroup.controls['confirmatoryDiagnosis'].value).toBe( + 'Type 2 Diabetes' + ); + expect(diagGroup.controls['confirmatoryDiagnosis'].disabled).toBeTrue(); + }); + + it('should not modify form when dialog returns null', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + diagGroup.controls['term'].setValue(''); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(diagGroup.controls['term'].value).toBe(''); + }); + + it('should not modify form when dialog returns undefined (falsy)', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['confirmatoryDiagnosis'].setValue('Diabetes mellitus'); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(undefined)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(diagGroup.controls['term'].value).toBe(''); + }); + }); + + describe('ngDoCheck', () => { + it('should call fetchLanguageResponse on each change detection cycle', () => { + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + const directive = buttonEl.injector.get(ConfirmatoryDiagnosisDirective); + spyOn(directive, 'fetchLanguageResponse'); + directive.ngDoCheck(); + expect(directive.fetchLanguageResponse).toHaveBeenCalled(); + }); + }); + + describe('fetchLanguageResponse', () => { + it('should update currentLanguageSet from HttpServiceService', () => { + languageSubject.next({ + confirmDiagnosis: 'Updated Diagnosis', + }); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(ConfirmatoryDiagnosisDirective) + )[1]; + const directive = buttonEl.injector.get(ConfirmatoryDiagnosisDirective); + directive.fetchLanguageResponse(); + + expect(directive.currentLanguageSet.confirmDiagnosis).toBe( + 'Updated Diagnosis' + ); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/disableFormControl.directive.spec.ts b/src/app/app-modules/core/directives/disableFormControl.directive.spec.ts new file mode 100644 index 00000000..98c5bf95 --- /dev/null +++ b/src/app/app-modules/core/directives/disableFormControl.directive.spec.ts @@ -0,0 +1,148 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { DisableFormControlDirective } from './disableFormControl.directive'; + +@Component({ + template: ` +
+ + +
+ `, +}) +class TestHostComponent { + shouldDisable = false; + form = new FormGroup({ + testControl: new FormControl('initial'), + // A sibling keeps the parent group enabled when testControl is disabled. + otherControl: new FormControl('other'), + }); +} + +describe('DisableFormControlDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DisableFormControlDirective, TestHostComponent], + imports: [ReactiveFormsModule], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create the directive', () => { + const inputEl = fixture.debugElement.query( + By.directive(DisableFormControlDirective) + ); + expect(inputEl).toBeTruthy(); + }); + + it('should keep the control enabled when condition is false', () => { + component.shouldDisable = false; + fixture.detectChanges(); + expect(component.form.controls['testControl'].enabled).toBeTrue(); + }); + + it('should disable the control and set value to null when condition is true', () => { + component.shouldDisable = true; + fixture.detectChanges(); + expect(component.form.controls['testControl'].disabled).toBeTrue(); + expect(component.form.controls['testControl'].value).toBeNull(); + }); + + it('should mark the control as touched when disabling', () => { + component.shouldDisable = true; + fixture.detectChanges(); + expect(component.form.controls['testControl'].touched).toBeTrue(); + }); + + it('should re-enable the control when condition changes back to false', () => { + component.shouldDisable = true; + fixture.detectChanges(); + expect(component.form.controls['testControl'].disabled).toBeTrue(); + + component.shouldDisable = false; + fixture.detectChanges(); + expect(component.form.controls['testControl'].enabled).toBeTrue(); + }); + + it('should mark the control as touched when enabling', () => { + component.shouldDisable = false; + fixture.detectChanges(); + component.form.controls['testControl'].markAsUntouched(); + + component.shouldDisable = true; + fixture.detectChanges(); + expect(component.form.controls['testControl'].touched).toBeTrue(); + }); + + it('should keep control disabled when parent form group is disabled', () => { + component.form.disable(); + fixture.detectChanges(); + + component.shouldDisable = false; + fixture.detectChanges(); + + expect(component.form.controls['testControl'].disabled).toBeTrue(); + }); + + it('should not set value to null when parent is disabled (action is disable due to parent)', () => { + component.form.controls['testControl'].setValue('keepMe'); + component.form.disable(); + fixture.detectChanges(); + + const control = component.form.controls['testControl']; + expect(control.disabled).toBeTrue(); + }); + + it('should not re-enable a lone control because its parent group becomes disabled with it', () => { + const lone = new FormGroup({ only: new FormControl('x') }); + const directive = new DisableFormControlDirective({ + control: lone.controls.only, + } as any); + directive.disableFormControl = true; + expect(lone.disabled).toBeTrue(); + directive.disableFormControl = false; + expect(lone.controls.only.disabled).toBeTrue(); + }); + + it('should do nothing when the control has no parent', () => { + const orphan = new FormControl('x'); + const directive = new DisableFormControlDirective({ + control: orphan, + } as any); + directive.disableFormControl = true; + expect(orphan.enabled).toBeTrue(); + expect(orphan.value).toBe('x'); + }); +}); diff --git a/src/app/app-modules/core/directives/email/myEmail.directive.spec.ts b/src/app/app-modules/core/directives/email/myEmail.directive.spec.ts new file mode 100644 index 00000000..3824cd37 --- /dev/null +++ b/src/app/app-modules/core/directives/email/myEmail.directive.spec.ts @@ -0,0 +1,155 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FormControl, + FormsModule, + NgControl, + ReactiveFormsModule, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { MyEmailDirective } from './myEmail.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent { + emailCtrl = new FormControl(''); +} + +describe('MyEmailDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let inputEl: DebugElement; + let directive: MyEmailDirective; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MyEmailDirective, TestHostComponent], + imports: [ReactiveFormsModule, FormsModule], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + inputEl = fixture.debugElement.query(By.directive(MyEmailDirective)); + directive = inputEl.injector.get(MyEmailDirective); + }); + + it('should create the directive', () => { + expect(directive).toBeTruthy(); + }); + + describe('validate', () => { + it('should return null for empty string', () => { + const control = new FormControl(''); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for null value', () => { + const control = new FormControl(null); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for valid .com email', () => { + const control = new FormControl('user@example.com'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for valid .org email', () => { + const control = new FormControl('test.user@company.org'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for valid .in email', () => { + const control = new FormControl('user@domain.in'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for valid .co.in email', () => { + const control = new FormControl('user@domain.co.in'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return null for valid .COM email (uppercase)', () => { + const control = new FormControl('user@domain.COM'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return { valid: false } for invalid email without @', () => { + const control = new FormControl('userexample.com'); + expect(directive.validate(control)).toEqual({ valid: false }); + }); + + it('should return { valid: false } for email with invalid TLD', () => { + const control = new FormControl('user@example.xyz'); + expect(directive.validate(control)).toEqual({ valid: false }); + }); + + it('should return { valid: false } for email with no domain', () => { + const control = new FormControl('user@.com'); + expect(directive.validate(control)).toEqual({ valid: false }); + }); + + it('should return { valid: false } for email with special chars in local part', () => { + const control = new FormControl('us!er@domain.com'); + expect(directive.validate(control)).toEqual({ valid: false }); + }); + + it('should allow underscores and dots in local part', () => { + const control = new FormControl('user_name.test@domain.com'); + expect(directive.validate(control)).toBeNull(); + }); + + it('should return { valid: false } for email with spaces', () => { + const control = new FormControl('user @domain.com'); + expect(directive.validate(control)).toEqual({ valid: false }); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/name/myName.directive.spec.ts b/src/app/app-modules/core/directives/name/myName.directive.spec.ts new file mode 100644 index 00000000..ad657743 --- /dev/null +++ b/src/app/app-modules/core/directives/name/myName.directive.spec.ts @@ -0,0 +1,128 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MyNameDirective } from './myName.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent {} + +describe('MyNameDirective', () => { + let fixture: ComponentFixture; + let inputEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MyNameDirective, TestHostComponent], + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + inputEl = fixture.debugElement.query(By.directive(MyNameDirective)); + }); + + it('should create the directive on the host element', () => { + expect(inputEl).toBeTruthy(); + }); + + describe('keypress - input filtering', () => { + function keypress(charCode: number, which: number) { + return { + charCode, + which, + preventDefault: jasmine.createSpy('preventDefault'), + }; + } + + it('does not currently block digit characters (malformed regex never matches a single char)', () => { + const event = keypress(49, 49); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('does not currently block special characters like @ (malformed regex never matches a single char)', () => { + const event = keypress(64, 64); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow alphabetic characters', () => { + const event = keypress(65, 65); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow lowercase alphabetic characters', () => { + const event = keypress(97, 97); + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should prevent default when the key matches the filter regex', () => { + const test = jasmine.createSpy('test').and.returnValue(true); + // jasmine restores window.RegExp after this spec + spyOn(window as any, 'RegExp').and.returnValue({ test }); + const event = keypress(49, 49); + inputEl.triggerEventHandler('keypress', event); + expect(test).toHaveBeenCalledWith(String.fromCharCode(49)); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should use ev.which when charCode is 0', () => { + const test = jasmine.createSpy('test').and.returnValue(true); + spyOn(window as any, 'RegExp').and.returnValue({ test }); + const event = keypress(0, 49); + inputEl.triggerEventHandler('keypress', event); + expect(test).toHaveBeenCalledWith(String.fromCharCode(49)); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/null-default-value.directive.spec.ts b/src/app/app-modules/core/directives/null-default-value.directive.spec.ts new file mode 100644 index 00000000..02394f20 --- /dev/null +++ b/src/app/app-modules/core/directives/null-default-value.directive.spec.ts @@ -0,0 +1,106 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { NullDefaultValueDirective } from './null-default-value.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent { + ctrl = new FormControl('initial'); +} + +describe('NullDefaultValueDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let inputEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [NullDefaultValueDirective, TestHostComponent], + imports: [ReactiveFormsModule], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + inputEl = fixture.debugElement.query( + By.directive(NullDefaultValueDirective) + ); + }); + + it('should create the directive', () => { + expect(inputEl).toBeTruthy(); + }); + + describe('input event - value conversion', () => { + it('should set control value to null when input is empty', () => { + const nativeInput = inputEl.nativeElement as HTMLInputElement; + nativeInput.value = ''; + nativeInput.dispatchEvent(new Event('input')); + fixture.detectChanges(); + expect(component.ctrl.value).toBeNull(); + }); + + it('should set control value to the input value when not empty', () => { + const nativeInput = inputEl.nativeElement as HTMLInputElement; + nativeInput.value = 'test'; + nativeInput.dispatchEvent(new Event('input')); + fixture.detectChanges(); + expect(component.ctrl.value).toBe('test'); + }); + + it('should set control value for a single character', () => { + const nativeInput = inputEl.nativeElement as HTMLInputElement; + nativeInput.value = 'a'; + nativeInput.dispatchEvent(new Event('input')); + fixture.detectChanges(); + expect(component.ctrl.value).toBe('a'); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = new Event('paste', { cancelable: true }); + spyOn(event, 'preventDefault'); + inputEl.nativeElement.dispatchEvent(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = new Event('copy', { cancelable: true }); + spyOn(event, 'preventDefault'); + inputEl.nativeElement.dispatchEvent(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = new Event('cut', { cancelable: true }); + spyOn(event, 'preventDefault'); + inputEl.nativeElement.dispatchEvent(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/numberValidator.directive.spec.ts b/src/app/app-modules/core/directives/numberValidator.directive.spec.ts new file mode 100644 index 00000000..4db76fe8 --- /dev/null +++ b/src/app/app-modules/core/directives/numberValidator.directive.spec.ts @@ -0,0 +1,150 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NumberValidatorDirective } from './numberValidator.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent {} + +describe('NumberValidatorDirective', () => { + let fixture: ComponentFixture; + let inputEl: DebugElement; + let directive: NumberValidatorDirective; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [NumberValidatorDirective, TestHostComponent], + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + inputEl = fixture.debugElement.query( + By.directive(NumberValidatorDirective) + ); + directive = inputEl.injector.get(NumberValidatorDirective); + }); + + it('should create the directive', () => { + expect(directive).toBeTruthy(); + }); + + it('should have allowMax set to "100"', () => { + expect(directive.allowMax).toBe('100'); + }); + + describe('validate', () => { + it('should return true for value less than allowMax', () => { + expect(directive.validate(50)).toBeTrue(); + }); + + it('should return true for value equal to allowMax', () => { + expect(directive.validate(100)).toBeTrue(); + }); + + it('should return false for value greater than allowMax', () => { + expect(directive.validate(101)).toBeFalse(); + }); + + it('should return true for zero', () => { + expect(directive.validate(0)).toBeTrue(); + }); + + it('should return true for negative values', () => { + expect(directive.validate(-5)).toBeTrue(); + }); + }); + + describe('onFocus', () => { + it('should store the current value as lastValue', () => { + const event = { target: { value: '42' } }; + directive.onFocus(event); + expect(directive.lastValue).toBe('42' as any); + }); + }); + + describe('onInput', () => { + it('should keep valid value', () => { + directive.lastValue = '50' as any; + const event = { target: { value: '75' } }; + directive.onInput(event); + expect(event.target.value).toBe('75'); + }); + + it('should revert to lastValue when value exceeds allowMax', () => { + directive.lastValue = '90' as any; + const event = { target: { value: '150' } }; + directive.onInput(event); + expect(event.target.value).toBe('90'); + }); + + it('should update lastValue after valid input', () => { + directive.lastValue = '10' as any; + const event = { target: { value: '50' } }; + directive.onInput(event); + expect(directive.lastValue).toBe('50' as any); + }); + + it('should update lastValue to the reverted value after invalid input', () => { + directive.lastValue = '80' as any; + const event = { target: { value: '200' } }; + directive.onInput(event); + expect(directive.lastValue).toBe('80' as any); + }); + + it('should allow value equal to max', () => { + directive.lastValue = '99' as any; + const event = { target: { value: '100' } }; + directive.onInput(event); + expect(event.target.value).toBe('100'); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/open-modal.directive.spec.ts b/src/app/app-modules/core/directives/open-modal.directive.spec.ts new file mode 100644 index 00000000..6a687ab0 --- /dev/null +++ b/src/app/app-modules/core/directives/open-modal.directive.spec.ts @@ -0,0 +1,285 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FormBuilder, + FormControl, + FormGroup, + FormArray, + ReactiveFormsModule, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { of, Subject, BehaviorSubject } from 'rxjs'; +import { OpenModalDirective } from './open-modal.directive'; +import { HttpServiceService } from '../services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +@Component({ + template: ` +
+
+
+ + +
+
+
+ `, +}) +class TestHostComponent { + previousSelected: any[] = []; + parentForm: FormGroup; + diagnosisGroup: FormGroup; + + constructor(private fb: FormBuilder) { + this.parentForm = this.fb.group({ + diagnosisList: this.fb.array([ + this.fb.group({ + provisionalDiagnosis: [''], + term: [''], + conceptID: [''], + }), + ]), + }); + const diagArray = this.parentForm.get('diagnosisList') as FormArray; + this.diagnosisGroup = diagArray.at(0) as FormGroup; + } +} + +describe('OpenModalDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let mockDialog: jasmine.SpyObj; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + let languageSubject: BehaviorSubject; + + beforeEach(() => { + languageSubject = new BehaviorSubject({ + DiagnosisDetails: { provisionaldiagnosis: 'Provisional Diagnosis' }, + }); + + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockHttpService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: languageSubject.asObservable(), + }); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + mockSessionStorage.getItem.and.returnValue(null); + + TestBed.configureTestingModule({ + declarations: [OpenModalDirective, TestHostComponent], + imports: [ReactiveFormsModule], + providers: [ + FormBuilder, + { provide: MatDialog, useValue: mockDialog }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create directives on host elements', () => { + const directives = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + ); + expect(directives.length).toBe(2); + }); + + it('should fetch language response on init', () => { + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + const directive = buttonEl.injector.get(OpenModalDirective); + expect(directive.currentLanguageSet).toBeDefined(); + expect( + directive.currentLanguageSet.DiagnosisDetails.provisionaldiagnosis + ).toBe('Provisional Diagnosis'); + }); + + describe('click handler', () => { + it('should open dialog when non-INPUT element is clicked', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(mockDialog.open).toHaveBeenCalled(); + }); + + it('should NOT open dialog when INPUT element is clicked', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + + const inputEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[0]; + inputEl.triggerEventHandler('click', null); + + expect(mockDialog.open).not.toHaveBeenCalled(); + }); + }); + + describe('keyup.enter handler', () => { + it('should open dialog on Enter key', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('keyup.enter', null); + + expect(mockDialog.open).toHaveBeenCalled(); + }); + }); + + describe('openDialog', () => { + it('should not open dialog when search term is 2 chars or less', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('ab'); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(mockDialog.open).not.toHaveBeenCalled(); + }); + + it('should pass correct data to dialog', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + component.previousSelected = [{ term: 'existing' }]; + fixture.detectChanges(); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + const callArgs: any[] = mockDialog.open.calls.mostRecent().args; + expect(callArgs[1]?.data.searchTerm).toBe('Diabetes mellitus'); + expect(callArgs[1]?.data.addedDiagnosis).toEqual([{ term: 'existing' }]); + expect(callArgs[1]?.width).toBe('800px'); + }); + + it('should handle dialog result with diagnosis data', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + + const result = [{ term: 'Type 2 Diabetes', conceptID: 'C001' }]; + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(result)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(diagGroup.controls['term'].value).toBe('Type 2 Diabetes'); + expect(diagGroup.controls['conceptID'].value).toBe('C001'); + expect(diagGroup.controls['provisionalDiagnosis'].value).toBe( + 'Type 2 Diabetes' + ); + expect(diagGroup.controls['provisionalDiagnosis'].disabled).toBeTrue(); + }); + + it('should not modify form when dialog returns null', () => { + const diagArray = component.parentForm.get('diagnosisList') as FormArray; + const diagGroup = diagArray.at(0) as FormGroup; + diagGroup.controls['provisionalDiagnosis'].setValue('Diabetes mellitus'); + diagGroup.controls['term'].setValue(''); + + const dialogRefSpy = jasmine.createSpyObj('MatDialogRef', [ + 'afterClosed', + ]); + dialogRefSpy.afterClosed.and.returnValue(of(null)); + mockDialog.open.and.returnValue(dialogRefSpy); + + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + buttonEl.triggerEventHandler('click', null); + + expect(diagGroup.controls['term'].value).toBe(''); + }); + }); + + describe('ngDoCheck', () => { + it('should call fetchLanguageResponse on each change detection cycle', () => { + const buttonEl = fixture.debugElement.queryAll( + By.directive(OpenModalDirective) + )[1]; + const directive = buttonEl.injector.get(OpenModalDirective); + spyOn(directive, 'fetchLanguageResponse'); + directive.ngDoCheck(); + expect(directive.fetchLanguageResponse).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/password/myPassword.directive.spec.ts b/src/app/app-modules/core/directives/password/myPassword.directive.spec.ts new file mode 100644 index 00000000..9522dd71 --- /dev/null +++ b/src/app/app-modules/core/directives/password/myPassword.directive.spec.ts @@ -0,0 +1,184 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MyPasswordDirective } from './myPassword.directive'; + +@Component({ + template: `
`, +}) +class TestHostComponent {} + +describe('MyPasswordDirective', () => { + let fixture: ComponentFixture; + let inputEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [MyPasswordDirective, TestHostComponent], + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + inputEl = fixture.debugElement.query(By.directive(MyPasswordDirective)); + }); + + it('should create the directive on the host element', () => { + expect(inputEl).toBeTruthy(); + }); + + describe('keyup - password validation feedback', () => { + function triggerKeyup(value: string): void { + const el = inputEl.nativeElement as HTMLInputElement; + el.value = value; + inputEl.triggerEventHandler('keyup', { + target: el, + }); + } + + it('should show "Strong Password" and green border for a strong password', () => { + triggerKeyup('Abcdef12'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(hint.textContent).toBe('Strong Password'); + expect(el.style.border).toBe('2px solid green'); + }); + + it('should show "Strong Password" for a 12-char valid password', () => { + triggerKeyup('Abcdefgh1234'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(hint.textContent).toBe('Strong Password'); + expect(el.style.border).toBe('2px solid green'); + }); + + it('should treat a 7-char password as invalid (the regex already enforces 8-12 chars)', () => { + triggerKeyup('Abcdefg'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(hint.textContent).toContain( + 'password should be 8-12 characters long' + ); + expect(el.style.border).toBe('2px solid red'); + }); + + it('should show "Weak Password" and yellow border when the validator reports weak', () => { + const directive = inputEl.injector.get(MyPasswordDirective); + const validator = spyOn( + directive as any, + 'passwordValidator' + ).and.returnValue(0); + triggerKeyup('Abcdefg'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(validator).toHaveBeenCalledWith('Abcdefg'); + expect(hint.textContent).toBe('Weak Password'); + expect(el.style.border).toBe('2px solid yellow'); + }); + + it('should show error message and red border for invalid password', () => { + triggerKeyup('123'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(hint.textContent).toContain( + 'password should be 8-12 characters long' + ); + expect(el.style.border).toBe('2px solid red'); + }); + + it('should show error when password starts with a number', () => { + triggerKeyup('1abcdefg'); + const el = inputEl.nativeElement as HTMLInputElement; + const hint = (el.nextSibling as any).nextElementSibling as HTMLElement; + expect(hint.textContent).toContain( + 'password should be 8-12 characters long' + ); + expect(el.style.border).toBe('2px solid red'); + }); + + it('should show error for empty password', () => { + triggerKeyup(''); + const el = inputEl.nativeElement as HTMLInputElement; + expect(el.style.border).toBe('2px solid red'); + }); + }); + + describe('keypress - whitespace prevention', () => { + it('should prevent space characters', () => { + const event = { + charCode: 32, + which: 32, + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should allow normal characters', () => { + const event = { + charCode: 65, + which: 65, + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should use ev.which when charCode is 0', () => { + const event = { + charCode: 0, + which: 65, + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('keypress', event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/directives/stringValidator.directive.spec.ts b/src/app/app-modules/core/directives/stringValidator.directive.spec.ts new file mode 100644 index 00000000..13751fbb --- /dev/null +++ b/src/app/app-modules/core/directives/stringValidator.directive.spec.ts @@ -0,0 +1,505 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { StringValidatorDirective } from './stringValidator.directive'; + +@Component({ + template: ``, +}) +class TestHostComponent { + validatorType = 'alphabet'; +} + +describe('StringValidatorDirective', () => { + let fixture: ComponentFixture; + let component: TestHostComponent; + let inputEl: DebugElement; + let directive: StringValidatorDirective; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StringValidatorDirective, TestHostComponent], + }); + fixture = TestBed.createComponent(TestHostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + inputEl = fixture.debugElement.query( + By.directive(StringValidatorDirective) + ); + directive = inputEl.injector.get(StringValidatorDirective); + }); + + it('should create the directive', () => { + expect(directive).toBeTruthy(); + }); + + describe('validate', () => { + it('should return false for null input', () => { + directive.allowText = 'alphabet'; + expect(directive.validate(null)).toBeFalse(); + }); + + it('should return false for empty string input', () => { + directive.allowText = 'alphabet'; + expect(directive.validate('')).toBeFalse(); + }); + + describe('alphabet', () => { + beforeEach(() => { + directive.allowText = 'alphabet'; + }); + + it('should return true for alphabetic input', () => { + expect(directive.validate('Hello')).toBeTrue(); + }); + + it('should return false for input with numbers', () => { + expect(directive.validate('Hello123')).toBeFalse(); + }); + + it('should return false for input with spaces', () => { + expect(directive.validate('Hello World')).toBeFalse(); + }); + }); + + describe('alphaspace', () => { + beforeEach(() => { + directive.allowText = 'alphaspace'; + }); + + it('should return true for alpha with spaces', () => { + expect(directive.validate('Hello World')).toBeTrue(); + }); + + it('should return false for input with numbers', () => { + expect(directive.validate('Hello 123')).toBeFalse(); + }); + }); + + describe('alphanumeric', () => { + beforeEach(() => { + directive.allowText = 'alphanumeric'; + }); + + it('should return true for alphanumeric input', () => { + expect(directive.validate('Hello123')).toBeTrue(); + }); + + it('should return false for input with spaces', () => { + expect(directive.validate('Hello 123')).toBeFalse(); + }); + + it('should return false for input with special chars', () => { + expect(directive.validate('Hello@123')).toBeFalse(); + }); + }); + + describe('alphanumericspace', () => { + beforeEach(() => { + directive.allowText = 'alphanumericspace'; + }); + + it('should return true for alphanumeric with spaces', () => { + expect(directive.validate('Hello 123')).toBeTrue(); + }); + + it('should return false for input with special chars', () => { + expect(directive.validate('Hello@123')).toBeFalse(); + }); + }); + + describe('number', () => { + beforeEach(() => { + directive.allowText = 'number'; + }); + + it('should return true for numeric input', () => { + expect(directive.validate('12345')).toBeTrue(); + }); + + it('should return false for alpha input', () => { + expect(directive.validate('abc')).toBeFalse(); + }); + }); + + describe('numberslash', () => { + beforeEach(() => { + directive.allowText = 'numberslash'; + }); + + it('should return true for numbers with slash', () => { + expect(directive.validate('123/456')).toBeTrue(); + }); + + it('should return false for alpha input', () => { + expect(directive.validate('abc')).toBeFalse(); + }); + }); + + describe('alphanumerichyphen', () => { + beforeEach(() => { + directive.allowText = 'alphanumerichyphen'; + }); + + it('should return true for alphanumeric with hyphen', () => { + expect(directive.validate('ABC-123/test')).toBeTrue(); + }); + + it('should return false for special chars', () => { + expect(directive.validate('ABC@123')).toBeFalse(); + }); + }); + + describe('numerichyphen', () => { + beforeEach(() => { + directive.allowText = 'numerichyphen'; + }); + + it('should return true for numbers with hyphen', () => { + expect(directive.validate('123-456')).toBeTrue(); + }); + + it('should return false for alpha input', () => { + expect(directive.validate('abc-123')).toBeFalse(); + }); + }); + + describe('decimal', () => { + beforeEach(() => { + directive.allowText = 'decimal'; + }); + + it('should return true for integer', () => { + expect(directive.validate('123')).toBeTrue(); + }); + + it('should return true for decimal with up to 2 decimal places', () => { + expect(directive.validate('123.45')).toBeTrue(); + }); + + it('should return false for decimal with 3 decimal places', () => { + expect(directive.validate('123.456')).toBeFalse(); + }); + + it('should return true for decimal with 1 decimal place', () => { + expect(directive.validate('1.5')).toBeTrue(); + }); + }); + + describe('address', () => { + beforeEach(() => { + directive.allowText = 'address'; + }); + + it('should return true for address text', () => { + expect(directive.validate('123 Main St. #4')).toBeTrue(); + }); + + it('should return false for special chars not in address', () => { + expect(directive.validate('addr@home')).toBeFalse(); + }); + }); + + describe('inputFieldValidator', () => { + beforeEach(() => { + directive.allowText = 'inputFieldValidator'; + }); + + it('should return true for alphanumeric with spaces', () => { + expect(directive.validate('Hello 123')).toBeTrue(); + }); + + it('should return false for special chars', () => { + expect(directive.validate('Hello!123')).toBeFalse(); + }); + }); + + describe('textAreaValidator', () => { + beforeEach(() => { + directive.allowText = 'textAreaValidator'; + }); + + it('should return true for text with dots and commas', () => { + expect(directive.validate('Hello, world. 123')).toBeTrue(); + }); + + it('should return false for special chars', () => { + expect(directive.validate('Hello@world')).toBeFalse(); + }); + }); + + describe('questionnaireValidator', () => { + beforeEach(() => { + directive.allowText = 'questionnaireValidator'; + }); + + it('should return true for text with question mark', () => { + expect(directive.validate('How are you?')).toBeTrue(); + }); + + it('should return false for special chars', () => { + expect(directive.validate('Hello@world')).toBeFalse(); + }); + }); + + describe('addressValidator', () => { + beforeEach(() => { + directive.allowText = 'addressValidator'; + }); + + it('should return true for address text', () => { + expect(directive.validate('123, Main St. #4-5/A')).toBeTrue(); + }); + }); + + describe('smsTemplateValidator', () => { + beforeEach(() => { + directive.allowText = 'smsTemplateValidator'; + }); + + it('should return true for SMS template text', () => { + expect( + directive.validate( + 'Hello, your balance is $100. Call: (123) 456-7890' + ) + ).toBeTrue(); + }); + }); + + describe('itemNameSearchValidator', () => { + beforeEach(() => { + directive.allowText = 'itemNameSearchValidator'; + }); + + it('should return true for text with percent', () => { + expect(directive.validate('Paracetamol 50%')).toBeTrue(); + }); + + it('should return false for special chars', () => { + expect(directive.validate('item@name')).toBeFalse(); + }); + }); + + describe('itemNameMasterValidator', () => { + beforeEach(() => { + directive.allowText = 'itemNameMasterValidator'; + }); + + it('should return true for item name with allowed chars', () => { + expect(directive.validate('Item-Name.50%(test)')).toBeTrue(); + }); + }); + + describe('answerValidator', () => { + beforeEach(() => { + directive.allowText = 'answerValidator'; + }); + + it('should return true for answer text', () => { + expect(directive.validate('Answer 1, option-2/3')).toBeTrue(); + }); + }); + + describe('usernameValidator', () => { + beforeEach(() => { + directive.allowText = 'usernameValidator'; + }); + + it('should return true for alphanumeric username', () => { + expect(directive.validate('user123')).toBeTrue(); + }); + + it('should return false for username with special chars', () => { + expect(directive.validate('user@123')).toBeFalse(); + }); + + it('should return false for username with spaces', () => { + expect(directive.validate('user 123')).toBeFalse(); + }); + }); + + describe('default case', () => { + it('should return false for unknown pattern code', () => { + directive.allowText = 'unknownPattern'; + expect(directive.validate('anything')).toBeFalse(); + }); + }); + }); + + describe('onFocus', () => { + it('should store current value as lastValue', () => { + const event = { target: { value: 'test' } }; + directive.onFocus(event); + expect(directive.lastValue).toBe('test' as any); + }); + }); + + describe('onInput', () => { + describe('decimal mode', () => { + beforeEach(() => { + directive.allowText = 'decimal'; + }); + + it('should clear value for empty input', () => { + directive.lastValue = '1' as any; + const event = { target: { value: '', maxLength: -1 } }; + directive.onInput(event); + expect(event.target.value).toBe(''); + }); + + it('should keep valid decimal', () => { + directive.lastValue = '1' as any; + const event = { target: { value: '12.34', maxLength: -1 } }; + directive.onInput(event); + expect(event.target.value).toBe('12.34'); + }); + + it('should revert invalid decimal', () => { + directive.lastValue = '12.34' as any; + const event = { target: { value: '12.345', maxLength: -1 } }; + directive.onInput(event); + expect(event.target.value).toBe('12.34'); + }); + }); + + describe('number mode', () => { + beforeEach(() => { + directive.allowText = 'number'; + }); + + it('should clear value when entering 0 with no length', () => { + directive.lastValue = '' as any; + const event = { target: { value: '0', maxLength: -1, length: 0 } }; + directive.onInput(event); + expect(event.target.value).toBe(''); + }); + + it('should allow valid number when length > 0', () => { + directive.lastValue = '1' as any; + const event = { target: { value: '12', maxLength: -1, length: 1 } }; + directive.onInput(event); + expect(event.target.value).toBe('12'); + }); + }); + + describe('other modes (e.g. alphabet)', () => { + beforeEach(() => { + directive.allowText = 'alphabet'; + }); + + it('should keep valid input', () => { + directive.lastValue = 'Hel' as any; + const event = { target: { value: 'Hell', maxLength: -1 } }; + directive.onInput(event); + expect(event.target.value).toBe('Hell'); + }); + + it('should revert invalid input', () => { + directive.lastValue = 'Hello' as any; + const event = { target: { value: 'Hello1', maxLength: -1 } }; + directive.onInput(event); + expect(event.target.value).toBe('Hello'); + }); + + it('should revert if value exceeds maxlength', () => { + directive.lastValue = 'Hel' as any; + const event = { target: { value: 'Hello', maxLength: 3 } }; + directive.onInput(event); + expect(event.target.value).toBe('Hel'); + }); + + it('should update lastValue after input', () => { + directive.lastValue = 'He' as any; + const event = { target: { value: 'Hel', maxLength: -1 } }; + directive.onInput(event); + expect(directive.lastValue).toBe('Hel' as any); + }); + }); + }); + + describe('findDelta', () => { + it('should find inserted character', () => { + expect(directive.findDelta('abcd', 'abc')).toBe('d'); + }); + + it('should find inserted character in the middle', () => { + expect(directive.findDelta('abXc', 'abc')).toBe('X'); + }); + + it('should return empty string when no difference', () => { + expect(directive.findDelta('abc', 'abc')).toBe(''); + }); + + it('should return empty string for removal case', () => { + expect(directive.findDelta('ab', 'abc')).toBe(''); + }); + }); + + describe('isValidChar', () => { + it('should delegate to validate', () => { + directive.allowText = 'alphabet'; + expect(directive.isValidChar('a')).toBeTrue(); + expect(directive.isValidChar('1')).toBeFalse(); + }); + }); + + describe('isValidString', () => { + it('should return true for all valid characters', () => { + directive.allowText = 'alphabet'; + expect(directive.isValidString('abc')).toBeTrue(); + }); + + it('should return false if any character is invalid', () => { + directive.allowText = 'alphabet'; + expect(directive.isValidString('ab1')).toBeFalse(); + }); + }); + + describe('clipboard events', () => { + it('should block paste', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('paste', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block copy', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('copy', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should block cut', () => { + const event = { + preventDefault: jasmine.createSpy('preventDefault'), + }; + inputEl.triggerEventHandler('cut', event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/services/auth-guard.service.spec.ts b/src/app/app-modules/core/services/auth-guard.service.spec.ts new file mode 100644 index 00000000..760c2a03 --- /dev/null +++ b/src/app/app-modules/core/services/auth-guard.service.spec.ts @@ -0,0 +1,141 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { Router, UrlTree } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { AuthGuard } from './auth-guard.service'; +import { AuthService } from './auth.service'; + +describe('AuthGuard', () => { + let guard: AuthGuard; + let authServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + let mockUrlTree: UrlTree; + + beforeEach(() => { + authServiceSpy = jasmine.createSpyObj('AuthService', [ + 'validateSessionKey', + ]); + routerSpy = jasmine.createSpyObj('Router', ['createUrlTree']); + mockUrlTree = {} as UrlTree; + routerSpy.createUrlTree.and.returnValue(mockUrlTree); + + TestBed.configureTestingModule({ + providers: [ + AuthGuard, + { provide: AuthService, useValue: authServiceSpy }, + { provide: Router, useValue: routerSpy }, + ], + }); + + guard = TestBed.inject(AuthGuard); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + describe('canActivate', () => { + const mockRoute = {} as any; + const mockState = {} as any; + + it('should return true when session is valid (statusCode 200 and data is truthy)', done => { + authServiceSpy.validateSessionKey.and.returnValue( + of({ statusCode: 200, data: { userId: 1 } }) + ); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(true); + done(); + }); + }); + + it('should redirect to /login when statusCode is not 200', done => { + authServiceSpy.validateSessionKey.and.returnValue( + of({ statusCode: 401, data: null }) + ); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + + it('should redirect to /login when data is falsy', done => { + authServiceSpy.validateSessionKey.and.returnValue( + of({ statusCode: 200, data: null }) + ); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + + it('should redirect to /login when response is null', done => { + authServiceSpy.validateSessionKey.and.returnValue(of(null as any)); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + + it('should redirect to /login when response is undefined', done => { + authServiceSpy.validateSessionKey.and.returnValue(of(undefined as any)); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + + it('should redirect to /login on HTTP error', done => { + authServiceSpy.validateSessionKey.and.returnValue( + throwError(() => new Error('Network error')) + ); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + + it('should redirect to /login when statusCode is 200 but data is empty string', done => { + authServiceSpy.validateSessionKey.and.returnValue( + of({ statusCode: 200, data: '' }) + ); + + guard.canActivate(mockRoute, mockState).subscribe((result: any) => { + expect(result).toBe(mockUrlTree); + expect(routerSpy.createUrlTree).toHaveBeenCalledWith(['/login']); + done(); + }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/auth.service.spec.ts b/src/app/app-modules/core/services/auth.service.spec.ts new file mode 100644 index 00000000..c21c4a92 --- /dev/null +++ b/src/app/app-modules/core/services/auth.service.spec.ts @@ -0,0 +1,281 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; +import { environment } from 'src/environments/environment'; + +describe('AuthService', () => { + let service: AuthService; + let httpMock: HttpTestingController; + let routerSpy: jasmine.SpyObj; + + beforeEach(() => { + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [AuthService, { provide: Router, useValue: routerSpy }], + }); + + service = TestBed.inject(AuthService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('login', () => { + it('should post login request without captchaToken when not provided', () => { + const userName = 'testUser'; + const password = 'testPass'; + const doLogout = false; + + service.login(userName, password, doLogout).subscribe((res: any) => { + expect(res).toEqual({ statusCode: 200 }); + }); + + const req = httpMock.expectOne(environment.loginUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + userName: 'testUser', + password: 'testPass', + doLogout: false, + withCredentials: true, + }); + req.flush({ statusCode: 200 }); + }); + + it('should include captchaToken in request body when provided', () => { + const userName = 'testUser'; + const password = 'testPass'; + const doLogout = true; + const captchaToken = 'abc123'; + + service.login(userName, password, doLogout, captchaToken).subscribe(); + + const req = httpMock.expectOne(environment.loginUrl); + expect(req.request.body.captchaToken).toBe('abc123'); + expect(req.request.body.doLogout).toBe(true); + req.flush({}); + }); + + it('should not include captchaToken when it is falsy', () => { + service.login('user', 'pass', false, '').subscribe(); + + const req = httpMock.expectOne(environment.loginUrl); + expect(req.request.body.captchaToken).toBeUndefined(); + req.flush({}); + }); + }); + + describe('userlogoutPreviousSession', () => { + it('should post the userName to the logout previous session URL', () => { + service.userlogoutPreviousSession('testUser').subscribe((res: any) => { + expect(res).toEqual({ status: 'ok' }); + }); + + const req = httpMock.expectOne(environment.userlogoutPreviousSessionUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ userName: 'testUser' }); + req.flush({ status: 'ok' }); + }); + }); + + describe('getUserSecurityQuestionsAnswer', () => { + it('should post lowercase userName', () => { + service + .getUserSecurityQuestionsAnswer('TestUser') + .subscribe((res: any) => { + expect(res).toEqual({ data: [] }); + }); + + const req = httpMock.expectOne( + environment.getUserSecurityQuestionsAnswerUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ userName: 'testuser' }); + req.flush({ data: [] }); + }); + }); + + describe('validateSecurityQuestionAndAnswer', () => { + it('should post security answers with lowercase userName', () => { + const answers = [{ questionId: 1, answer: 'answer1' }]; + + service + .validateSecurityQuestionAndAnswer(answers, 'TestUser') + .subscribe((res: any) => { + expect(res).toBeTruthy(); + }); + + const req = httpMock.expectOne( + environment.validateSecurityQuestionAndAnswerUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + SecurityQuesAns: answers, + userName: 'testuser', + }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('getTransactionIdForChangePassword', () => { + it('should post lowercase userName', () => { + service + .getTransactionIdForChangePassword('Admin') + .subscribe((res: any) => { + expect(res).toBeTruthy(); + }); + + const req = httpMock.expectOne( + environment.getTransactionIdForChangePasswordUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ userName: 'admin' }); + req.flush({ transactionId: 'txn123' }); + }); + }); + + describe('getSecurityQuestions', () => { + it('should make a GET request to the security question URL', () => { + service.getSecurityQuestions().subscribe((res: any) => { + expect(res.length).toBe(2); + }); + + const req = httpMock.expectOne(environment.getSecurityQuestionUrl); + expect(req.request.method).toBe('GET'); + req.flush([{ id: 1 }, { id: 2 }]); + }); + }); + + describe('saveUserSecurityQuestionsAnswer', () => { + it('should post user question-answer data', () => { + const qaPair = { questionId: 1, answer: 'test' }; + + service.saveUserSecurityQuestionsAnswer(qaPair).subscribe((res: any) => { + expect(res).toEqual({ status: 'saved' }); + }); + + const req = httpMock.expectOne( + environment.saveUserSecurityQuestionsAnswerUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(qaPair); + req.flush({ status: 'saved' }); + }); + }); + + describe('setNewPassword', () => { + it('should post the new password with transactionId from service property', () => { + service.transactionId = 'txn-456'; + + service + .setNewPassword('user1', 'newPass', 'ignored') + .subscribe((res: any) => { + expect(res).toBeTruthy(); + }); + + const req = httpMock.expectOne(environment.setNewPasswordUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + userName: 'user1', + password: 'newPass', + transactionId: 'txn-456', + }); + req.flush({ statusCode: 200 }); + }); + + it('should use undefined transactionId when not set', () => { + service.transactionId = undefined; + + service.setNewPassword('user1', 'pass', 'any').subscribe(); + + const req = httpMock.expectOne(environment.setNewPasswordUrl); + expect(req.request.body.transactionId).toBeUndefined(); + req.flush({}); + }); + }); + + describe('validateSessionKey', () => { + it('should post to session exists URL with empty body', () => { + service.validateSessionKey().subscribe((res: any) => { + expect(res).toEqual({ statusCode: 200, data: true }); + }); + + const req = httpMock.expectOne(environment.getSessionExistsURL); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush({ statusCode: 200, data: true }); + }); + }); + + describe('logout', () => { + it('should post empty string to logout URL', () => { + service.logout().subscribe((res: any) => { + expect(res).toBeTruthy(); + }); + + const req = httpMock.expectOne(environment.logoutUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toBe(''); + req.flush({ status: 'logged out' }); + }); + }); + + describe('getUIVersionAndCommitDetails', () => { + it('should make a GET request to the given URL', () => { + const url = 'https://example.com/version.json'; + + service.getUIVersionAndCommitDetails(url).subscribe((res: any) => { + expect(res.version).toBe('1.0'); + }); + + const req = httpMock.expectOne(url); + expect(req.request.method).toBe('GET'); + req.flush({ version: '1.0' }); + }); + }); + + describe('getAPIVersionAndCommitDetails', () => { + it('should make a GET request to the API version URL', () => { + service.getAPIVersionAndCommitDetails().subscribe((res: any) => { + expect(res.version).toBe('2.0'); + }); + + const req = httpMock.expectOne(environment.apiVersionUrl); + expect(req.request.method).toBe('GET'); + req.flush({ version: '2.0' }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/beneficiary-details.service.spec.ts b/src/app/app-modules/core/services/beneficiary-details.service.spec.ts new file mode 100644 index 00000000..03f879cb --- /dev/null +++ b/src/app/app-modules/core/services/beneficiary-details.service.spec.ts @@ -0,0 +1,216 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { BeneficiaryDetailsService } from './beneficiary-details.service'; +import { environment } from 'src/environments/environment'; + +describe('BeneficiaryDetailsService', () => { + let service: BeneficiaryDetailsService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [BeneficiaryDetailsService], + }); + + service = TestBed.inject(BeneficiaryDetailsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('initial state', () => { + it('should have beneficiaryDetails$ emitting null initially', done => { + service.beneficiaryDetails$.subscribe(val => { + expect(val).toBeNull(); + done(); + }); + }); + + it('should have HRPPositive as empty string initially', () => { + expect(service.HRPPositive).toBe(''); + }); + + it('should have HRPPositiveFlag$ emitting empty string initially', done => { + service.HRPPositiveFlag$.subscribe(val => { + expect(val).toBe(''); + done(); + }); + }); + }); + + describe('getBeneficiaryDetails', () => { + it('should post to correct URL and emit beneficiary data on success', done => { + const mockData = { name: 'John Doe', age: 30 }; + + // Skip the initial null emission + let emissionCount = 0; + service.beneficiaryDetails$.subscribe(data => { + emissionCount++; + if (emissionCount === 2) { + expect(data).toEqual(mockData); + done(); + } + }); + + service.getBeneficiaryDetails('REG123', 'FLOW456'); + + const req = httpMock.expectOne(environment.getBeneficiaryDetail); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + beneficiaryRegID: 'REG123', + benFlowID: 'FLOW456', + }); + req.flush({ data: mockData }); + }); + + it('should emit null when response has no data field', () => { + const emissions: any[] = []; + service.beneficiaryDetails$.subscribe(data => { + emissions.push(data); + }); + + service.getBeneficiaryDetails('REG1', 'FLOW1'); + + const req = httpMock.expectOne(environment.getBeneficiaryDetail); + req.flush({ data: null }); + + // Should only have the initial null emission (no new emission for falsy data) + expect(emissions.length).toBe(1); + expect(emissions[0]).toBeNull(); + }); + + it('should emit null on HTTP error', done => { + let emissionCount = 0; + service.beneficiaryDetails$.subscribe(data => { + emissionCount++; + if (emissionCount === 2) { + expect(data).toBeNull(); + done(); + } + }); + + service.getBeneficiaryDetails('REG1', 'FLOW1'); + + const req = httpMock.expectOne(environment.getBeneficiaryDetail); + req.error(new ProgressEvent('Network error'), { status: 500 }); + }); + + it('should emit null when response data is undefined', () => { + const emissions: any[] = []; + service.beneficiaryDetails$.subscribe(data => { + emissions.push(data); + }); + + service.getBeneficiaryDetails('REG1', 'FLOW1'); + + const req = httpMock.expectOne(environment.getBeneficiaryDetail); + req.flush({}); + + // No new emission since data is undefined (falsy) + expect(emissions.length).toBe(1); + }); + }); + + describe('getBeneficiaryImage', () => { + it('should return an observable from POST request', () => { + const mockResponse = { image: 'base64data' }; + + service.getBeneficiaryImage('REG123').subscribe((res: any) => { + expect(res).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(environment.getBeneficiaryImage); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ beneficiaryRegID: 'REG123' }); + req.flush(mockResponse); + }); + }); + + describe('reset', () => { + it('should emit null on beneficiaryDetails', done => { + // First set some data + service.beneficiaryDetails.next({ name: 'Test' }); + + // Now reset + service.reset(); + + service.beneficiaryDetails$.subscribe(data => { + expect(data).toBeNull(); + done(); + }); + }); + }); + + describe('setHRPPositive', () => { + it('should set HRPPositive to 1', () => { + service.setHRPPositive(); + expect(service.HRPPositive).toBe(1); + }); + + it('should emit 1 on HRPPositiveFlag$', done => { + let emissionCount = 0; + service.HRPPositiveFlag$.subscribe(val => { + emissionCount++; + if (emissionCount === 2) { + expect(val).toBe(1); + done(); + } + }); + + service.setHRPPositive(); + }); + }); + + describe('resetHRPPositive', () => { + it('should set HRPPositive to 0', () => { + service.HRPPositive = 1; + service.resetHRPPositive(); + expect(service.HRPPositive).toBe(0); + }); + + it('should emit 0 on HRPPositiveFlag$', done => { + let emissionCount = 0; + service.HRPPositiveFlag$.subscribe(val => { + emissionCount++; + if (emissionCount === 2) { + expect(val).toBe(0); + done(); + } + }); + + service.resetHRPPositive(); + }); + }); +}); diff --git a/src/app/app-modules/core/services/camera.service.spec.ts b/src/app/app-modules/core/services/camera.service.spec.ts new file mode 100644 index 00000000..e15af64d --- /dev/null +++ b/src/app/app-modules/core/services/camera.service.spec.ts @@ -0,0 +1,222 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { DOCUMENT } from '@angular/common'; +import { of } from 'rxjs'; +import { CameraService } from './camera.service'; +import { CameraDialogComponent } from '../components/camera-dialog/camera-dialog.component'; + +describe('CameraService', () => { + let service: CameraService; + let mockDialog: jasmine.SpyObj; + let mockDialogRef: jasmine.SpyObj>; + let mockComponentInstance: any; + + beforeEach(() => { + mockComponentInstance = { + capture: false, + imageCode: false, + annotate: false, + current_language_set: null, + availablePoints: null, + graph: null, + }; + + mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); + mockDialogRef.afterClosed.and.returnValue(of('result')); + mockDialogRef.componentInstance = mockComponentInstance; + + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockDialog.open.and.returnValue(mockDialogRef); + + TestBed.configureTestingModule({ + providers: [ + CameraService, + { provide: MatDialog, useValue: mockDialog }, + { provide: DOCUMENT, useValue: document }, + ], + }); + + service = TestBed.inject(CameraService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#capture', () => { + it('should open dialog with CameraDialogComponent', () => { + service.capture(); + expect(mockDialog.open).toHaveBeenCalledWith( + CameraDialogComponent, + jasmine.any(Object) + ); + }); + + it('should set capture to true on component instance', () => { + service.capture(); + expect(mockComponentInstance.capture).toBe(true); + }); + + it('should set imageCode to false on component instance', () => { + service.capture(); + expect(mockComponentInstance.imageCode).toBe(false); + }); + + it('should return the afterClosed observable', (done: DoneFn) => { + service.capture().subscribe(result => { + expect(result).toBe('result'); + done(); + }); + }); + + it('should accept optional titleAlign parameter', () => { + service.capture('left'); + expect(mockDialog.open).toHaveBeenCalled(); + }); + + it('should use center as default titleAlign', () => { + service.capture(); + expect(mockDialog.open).toHaveBeenCalled(); + }); + }); + + describe('#viewImage', () => { + it('should open dialog with CameraDialogComponent', () => { + service.viewImage('testCode'); + expect(mockDialog.open).toHaveBeenCalledWith( + CameraDialogComponent, + jasmine.any(Object) + ); + }); + + it('should set capture to false on component instance', () => { + service.viewImage('testCode'); + expect(mockComponentInstance.capture).toBe(false); + }); + + it('should set imageCode to the provided benImageCode', () => { + service.viewImage('imageCode123'); + expect(mockComponentInstance.imageCode).toBe('imageCode123'); + }); + + it('should accept optional titleAlign parameter', () => { + service.viewImage('testCode', 'left'); + expect(mockDialog.open).toHaveBeenCalled(); + }); + + it('should return void', () => { + const result = service.viewImage('testCode'); + expect(result).toBeUndefined(); + }); + }); + + describe('#annotate', () => { + it('should open dialog with 80% width', () => { + service.annotate('imageData', [], 'en'); + expect(mockDialog.open).toHaveBeenCalledWith(CameraDialogComponent, { + width: '80%', + }); + }); + + it('should set capture to false', () => { + service.annotate('imageData', [], 'en'); + expect(mockComponentInstance.capture).toBe(false); + }); + + it('should set imageCode to false', () => { + service.annotate('imageData', [], 'en'); + expect(mockComponentInstance.imageCode).toBe(false); + }); + + it('should set annotate to the provided image', () => { + service.annotate('base64image', [], 'en'); + expect(mockComponentInstance.annotate).toBe('base64image'); + }); + + it('should set current_language_set to the provided language', () => { + const langSet = { currentLanguage: 'English' }; + service.annotate('imageData', [], langSet); + expect(mockComponentInstance.current_language_set).toBe(langSet); + }); + + it('should set availablePoints to the provided points', () => { + const points = [{ x: 10, y: 20 }]; + service.annotate('imageData', points, 'en'); + expect(mockComponentInstance.availablePoints).toEqual(points); + }); + + it('should return the afterClosed observable', (done: DoneFn) => { + service.annotate('imageData', [], 'en').subscribe(result => { + expect(result).toBe('result'); + done(); + }); + }); + + it('should accept optional titleAlign parameter', () => { + service.annotate('imageData', [], 'en', 'left'); + expect(mockDialog.open).toHaveBeenCalled(); + }); + }); + + describe('#ViewGraph', () => { + it('should open dialog with 80% width', () => { + service.ViewGraph({ data: 'graphData' }); + expect(mockDialog.open).toHaveBeenCalledWith(CameraDialogComponent, { + width: '80%', + }); + }); + + it('should set capture to false', () => { + service.ViewGraph({}); + expect(mockComponentInstance.capture).toBe(false); + }); + + it('should set imageCode to false', () => { + service.ViewGraph({}); + expect(mockComponentInstance.imageCode).toBe(false); + }); + + it('should set annotate to false', () => { + service.ViewGraph({}); + expect(mockComponentInstance.annotate).toBe(false); + }); + + it('should set availablePoints to false', () => { + service.ViewGraph({}); + expect(mockComponentInstance.availablePoints).toBe(false); + }); + + it('should set graph to the provided graph data', () => { + const graphData = { series: [1, 2, 3] }; + service.ViewGraph(graphData); + expect(mockComponentInstance.graph).toBe(graphData); + }); + + it('should return void', () => { + const result = service.ViewGraph({}); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/src/app/app-modules/core/services/can-deactivate-guard.service.spec.ts b/src/app/app-modules/core/services/can-deactivate-guard.service.spec.ts new file mode 100644 index 00000000..285ace51 --- /dev/null +++ b/src/app/app-modules/core/services/can-deactivate-guard.service.spec.ts @@ -0,0 +1,121 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { Observable, of } from 'rxjs'; +import { + CanDeactivateGuardService, + CanComponentDeactivate, +} from './can-deactivate-guard.service'; + +describe('CanDeactivateGuardService', () => { + let service: CanDeactivateGuardService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [CanDeactivateGuardService], + }); + service = TestBed.inject(CanDeactivateGuardService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#canDeactivate', () => { + it('should return true when component has no canDeactivate method', () => { + const component = {} as CanComponentDeactivate; + const result = service.canDeactivate(component); + expect(result).toBe(true); + }); + + it('should call component canDeactivate when it exists and returns true', () => { + const component: CanComponentDeactivate = { + canDeactivate: () => true, + }; + spyOn(component, 'canDeactivate').and.returnValue(true); + const result = service.canDeactivate(component); + expect(component.canDeactivate).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should call component canDeactivate when it exists and returns false', () => { + const component: CanComponentDeactivate = { + canDeactivate: () => false, + }; + const result = service.canDeactivate(component); + expect(result).toBe(false); + }); + + it('should handle component canDeactivate returning an Observable', (done: DoneFn) => { + const component: CanComponentDeactivate = { + canDeactivate: () => of(true), + }; + const result = service.canDeactivate(component) as Observable; + result.subscribe(value => { + expect(value).toBe(true); + done(); + }); + }); + + it('should handle component canDeactivate returning Observable', (done: DoneFn) => { + const component: CanComponentDeactivate = { + canDeactivate: () => of(false), + }; + const result = service.canDeactivate(component) as Observable; + result.subscribe(value => { + expect(value).toBe(false); + done(); + }); + }); + + it('should handle component canDeactivate returning a Promise', async () => { + const component: CanComponentDeactivate = { + canDeactivate: () => Promise.resolve(true), + }; + const result = service.canDeactivate(component) as Promise; + const value = await result; + expect(value).toBe(true); + }); + + it('should handle component canDeactivate returning a rejected Promise', async () => { + const component: CanComponentDeactivate = { + canDeactivate: () => Promise.resolve(false), + }; + const result = service.canDeactivate(component) as Promise; + const value = await result; + expect(value).toBe(false); + }); + + it('should return true when canDeactivate is undefined', () => { + const component = { canDeactivate: undefined } as any; + const result = service.canDeactivate(component); + expect(result).toBe(true); + }); + + it('should return true when canDeactivate is null', () => { + const component = { canDeactivate: null } as any; + const result = service.canDeactivate(component); + expect(result).toBe(true); + }); + }); +}); diff --git a/src/app/app-modules/core/services/common-services.service.spec.ts b/src/app/app-modules/core/services/common-services.service.spec.ts new file mode 100644 index 00000000..e32c6585 --- /dev/null +++ b/src/app/app-modules/core/services/common-services.service.spec.ts @@ -0,0 +1,145 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { CommonService } from './common-services.service'; +import { environment } from 'src/environments/environment'; + +describe('CommonService', () => { + let service: CommonService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [CommonService], + }); + service = TestBed.inject(CommonService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('Subject and Observable', () => { + it('should have a commonServices Subject', () => { + expect(service.commonServices).toBeDefined(); + }); + + it('should have a commonServices$ observable', () => { + expect(service.commonServices$).toBeDefined(); + }); + + it('should emit values through commonServices$', (done: DoneFn) => { + const testData = { key: 'value' }; + service.commonServices$.subscribe(data => { + expect(data).toEqual(testData); + done(); + }); + service.commonServices.next(testData); + }); + }); + + describe('URL properties', () => { + it('should have getStatesURL from environment', () => { + expect(service.getStatesURL).toBe(environment.getStatesURL); + }); + + it('should have getDistrictsURL from environment', () => { + expect(service.getDistrictsURL).toBe(environment.getDistrictsURL); + }); + }); + + describe('#getStates', () => { + it('should make a GET request to getStatesURL', () => { + const mockResponse = { data: [{ stateId: 1, stateName: 'TestState' }] }; + + service.getStates(1).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(environment.getStatesURL); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + }); + + it('should accept any countryId parameter', () => { + service.getStates(99).subscribe(); + + const req = httpMock.expectOne(environment.getStatesURL); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + describe('#getDistricts', () => { + it('should make a GET request to getDistrictsURL with stateId appended', () => { + const stateId = 5; + const mockResponse = { + data: [{ districtId: 1, districtName: 'TestDistrict' }], + }; + + service.getDistricts(stateId).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(environment.getDistrictsURL + stateId); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + }); + + it('should handle different stateId values', () => { + const stateId = 100; + + service.getDistricts(stateId).subscribe(); + + const req = httpMock.expectOne(environment.getDistrictsURL + stateId); + expect(req.request.method).toBe('GET'); + req.flush([]); + }); + + it('should handle HTTP error responses', () => { + const stateId = 1; + + service.getDistricts(stateId).subscribe({ + error: error => { + expect(error.status).toBe(500); + }, + }); + + const req = httpMock.expectOne(environment.getDistrictsURL + stateId); + req.flush('Server Error', { + status: 500, + statusText: 'Internal Server Error', + }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/confirmation.service.spec.ts b/src/app/app-modules/core/services/confirmation.service.spec.ts new file mode 100644 index 00000000..6abfbbfc --- /dev/null +++ b/src/app/app-modules/core/services/confirmation.service.spec.ts @@ -0,0 +1,444 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { DOCUMENT } from '@angular/common'; +import { of } from 'rxjs'; +import { ConfirmationService } from './confirmation.service'; +import { CommonDialogComponent } from '../components/common-dialog/common-dialog.component'; + +describe('ConfirmationService', () => { + let service: ConfirmationService; + let dialogSpy: jasmine.SpyObj; + let mockDialogRef: any; + + beforeEach(() => { + mockDialogRef = { + componentInstance: { + title: '', + message: '', + status: '', + btnOkText: '', + btnCancelText: '', + confirmAlert: false, + confirmcalibration: false, + alert: false, + remarks: false, + editRemarks: false, + comments: '', + notify: false, + mandatories: null, + choice: false, + values: null, + choiceSelect: false, + sessionTimeout: false, + updateTimer: jasmine.createSpy('updateTimer'), + confirmHealthID: false, + alertFetsenseMessage: false, + confirmCBAC: false, + cbacData: null, + confirmCareContext: false, + }, + afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(true)), + disableClose: false, + }; + + dialogSpy = jasmine.createSpyObj('MatDialog', ['open']); + dialogSpy.open.and.returnValue(mockDialogRef); + + TestBed.configureTestingModule({ + providers: [ + ConfirmationService, + { provide: MatDialog, useValue: dialogSpy }, + { provide: DOCUMENT, useValue: document }, + ], + }); + + service = TestBed.inject(ConfirmationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('confirm', () => { + it('should open dialog with correct default parameters', () => { + service.confirm('Test Title', 'Test Message'); + + expect(dialogSpy.open).toHaveBeenCalledWith(CommonDialogComponent, { + width: '420px', + disableClose: false, + }); + expect(mockDialogRef.componentInstance.title).toBe('Test Title'); + expect(mockDialogRef.componentInstance.message).toBe('Test Message'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('OK'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(true); + expect(mockDialogRef.componentInstance.confirmcalibration).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + expect(mockDialogRef.componentInstance.remarks).toBe(false); + expect(mockDialogRef.componentInstance.editRemarks).toBe(false); + expect(mockDialogRef.disableClose).toBe(true); + }); + + it('should use custom OK and Cancel text', () => { + service.confirm('Title', 'Msg', 'Yes', 'No'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Yes'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('No'); + }); + + it('should return afterClosed observable', done => { + service.confirm('Title', 'Msg').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('confirmHealthId', () => { + it('should open dialog with confirmHealthID flag set', () => { + service.confirmHealthId('Health Title', 'Health Msg'); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.title).toBe('Health Title'); + expect(mockDialogRef.componentInstance.message).toBe('Health Msg'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('OK'); + expect(mockDialogRef.componentInstance.confirmHealthID).toBe(true); + expect(mockDialogRef.componentInstance.confirmcalibration).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + expect(mockDialogRef.disableClose).toBe(true); + }); + + it('should accept custom OK text', () => { + service.confirmHealthId('Title', 'Msg', 'Proceed'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Proceed'); + }); + + it('should return afterClosed observable', done => { + service.confirmHealthId('Title', 'Msg').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('alert', () => { + it('should open dialog with alert flag set and default parameters', () => { + const result = service.alert('Alert message'); + + expect(dialogSpy.open).toHaveBeenCalledWith(CommonDialogComponent, { + width: '420px', + }); + expect(mockDialogRef.componentInstance.message).toBe('Alert message'); + expect(mockDialogRef.componentInstance.status).toBe('info'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('OK'); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(true); + expect(mockDialogRef.componentInstance.remarks).toBe(false); + expect(mockDialogRef.componentInstance.editRemarks).toBe(false); + expect(result).toBe(mockDialogRef); + }); + + it('should convert status to lowercase', () => { + service.alert('Msg', 'ERROR'); + + expect(mockDialogRef.componentInstance.status).toBe('error'); + }); + + it('should accept custom OK text', () => { + service.alert('Msg', 'info', 'Got it'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Got it'); + }); + + it('should return the MatDialogRef directly (not afterClosed)', () => { + const ref = service.alert('test'); + expect(ref).toBe(mockDialogRef); + }); + }); + + describe('remarks', () => { + it('should open dialog with remarks flag set', () => { + service.remarks('Enter remarks'); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.message).toBe('Enter remarks'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Submit'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + expect(mockDialogRef.componentInstance.remarks).toBe(true); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + expect(mockDialogRef.componentInstance.editRemarks).toBe(false); + }); + + it('should accept custom parameters', () => { + service.remarks('Msg', 'left', 'right', 'Save', 'Dismiss'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Save'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Dismiss'); + }); + + it('should return afterClosed observable', done => { + service.remarks('Msg').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('editRemarks', () => { + it('should open dialog with editRemarks flag and comments', () => { + service.editRemarks('Edit', 'Previous comment'); + + expect(dialogSpy.open).toHaveBeenCalledWith(CommonDialogComponent, { + width: '60%', + }); + expect(mockDialogRef.componentInstance.message).toBe('Edit'); + expect(mockDialogRef.componentInstance.comments).toBe('Previous comment'); + expect(mockDialogRef.componentInstance.editRemarks).toBe(true); + expect(mockDialogRef.componentInstance.remarks).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + }); + + it('should accept custom button text', () => { + service.editRemarks('Msg', 'comments', 'l', 'r', 'Update', 'Back'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Update'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Back'); + }); + + it('should return afterClosed observable', done => { + service.editRemarks('Msg', 'comments').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('notify', () => { + it('should open dialog with notify flag and mandatories', () => { + const mandatories = ['field1', 'field2']; + service.notify('Missing fields', mandatories); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.message).toBe('Missing fields'); + expect(mockDialogRef.componentInstance.notify).toBe(true); + expect(mockDialogRef.componentInstance.mandatories).toEqual(mandatories); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + }); + + it('should return afterClosed observable', done => { + service.notify('Msg', []).subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('choice', () => { + it('should open dialog with choice flag and values', () => { + const values = ['option1', 'option2']; + service.choice('Choose one', values); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.message).toBe('Choose one'); + expect(mockDialogRef.componentInstance.choice).toBe(true); + expect(mockDialogRef.componentInstance.values).toEqual(values); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Confirm'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + expect(mockDialogRef.componentInstance.notify).toBe(false); + }); + + it('should accept custom button text', () => { + service.choice('Msg', [], 'c', 'c', 'Pick', 'Skip'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Pick'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Skip'); + }); + + it('should return afterClosed observable', done => { + service.choice('Msg', []).subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('startTimer', () => { + it('should open dialog with sessionTimeout and call updateTimer', () => { + service.startTimer('Session', 'Expiring', 300); + + expect(dialogSpy.open).toHaveBeenCalledWith(CommonDialogComponent, { + width: '420px', + disableClose: true, + }); + expect(mockDialogRef.componentInstance.title).toBe('Session'); + expect(mockDialogRef.componentInstance.message).toBe('Expiring'); + expect(mockDialogRef.componentInstance.sessionTimeout).toBe(true); + expect(mockDialogRef.componentInstance.updateTimer).toHaveBeenCalledWith( + 300 + ); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Continue'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + }); + + it('should return afterClosed observable', done => { + service.startTimer('T', 'M', 60).subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('choiceSelect', () => { + it('should open dialog with choiceSelect flag and values', () => { + const values = [{ id: 1, name: 'A' }]; + service.choiceSelect('Select', values); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.message).toBe('Select'); + expect(mockDialogRef.componentInstance.choiceSelect).toBe(true); + expect(mockDialogRef.componentInstance.choice).toBe(false); + expect(mockDialogRef.componentInstance.values).toEqual(values); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Proceed'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + }); + + it('should return afterClosed observable', done => { + service.choiceSelect('Msg', []).subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('alertFetsenseMessage', () => { + it('should open dialog with alertFetsenseMessage flag', () => { + service.alertFetsenseMessage('Fetosense connected'); + + expect(dialogSpy.open).toHaveBeenCalledWith(CommonDialogComponent, { + width: '420px', + }); + expect(mockDialogRef.componentInstance.message).toBe( + 'Fetosense connected' + ); + expect(mockDialogRef.componentInstance.status).toBe('Fetosense Device'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('OK'); + expect(mockDialogRef.componentInstance.alertFetsenseMessage).toBe(true); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.remarks).toBe(false); + }); + + it('should accept custom status and button text', () => { + service.alertFetsenseMessage('Msg', 'Custom Status', 'Close'); + + expect(mockDialogRef.componentInstance.status).toBe('Custom Status'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Close'); + }); + + it('should not return anything (void)', () => { + const result = service.alertFetsenseMessage('msg'); + expect(result).toBeUndefined(); + }); + }); + + describe('confirmCalibration', () => { + it('should open dialog with confirmcalibration flag', () => { + service.confirmCalibration('Calibrate?', 'Please calibrate'); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.title).toBe('Calibrate?'); + expect(mockDialogRef.componentInstance.message).toBe('Please calibrate'); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Yes'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('No'); + expect(mockDialogRef.componentInstance.confirmcalibration).toBe(true); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + }); + + it('should return afterClosed observable', done => { + service.confirmCalibration('T', 'M').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('confirmCBAC', () => { + it('should open dialog with confirmCBAC flag and data', () => { + const cbacData = { score: 5 }; + service.confirmCBAC('CBAC', 'Review CBAC', cbacData); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.title).toBe('CBAC'); + expect(mockDialogRef.componentInstance.message).toBe('Review CBAC'); + expect(mockDialogRef.componentInstance.confirmCBAC).toBe(true); + expect(mockDialogRef.componentInstance.cbacData).toEqual(cbacData); + expect(mockDialogRef.componentInstance.btnOkText).toBe('OK'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Cancel'); + }); + + it('should accept custom button text', () => { + service.confirmCBAC('T', 'M', {}, 'Proceed', 'Dismiss'); + + expect(mockDialogRef.componentInstance.btnOkText).toBe('Proceed'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('Dismiss'); + }); + + it('should return afterClosed observable', done => { + service.confirmCBAC('T', 'M', {}).subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); + + describe('confirmCareContext', () => { + it('should open dialog with confirmCareContext flag', () => { + service.confirmCareContext('Care Context', 'Confirm context'); + + expect(dialogSpy.open).toHaveBeenCalled(); + expect(mockDialogRef.componentInstance.title).toBe('Care Context'); + expect(mockDialogRef.componentInstance.message).toBe('Confirm context'); + expect(mockDialogRef.componentInstance.confirmCareContext).toBe(true); + expect(mockDialogRef.componentInstance.confirmCBAC).toBe(false); + expect(mockDialogRef.componentInstance.confirmcalibration).toBe(false); + expect(mockDialogRef.componentInstance.confirmAlert).toBe(false); + expect(mockDialogRef.componentInstance.alert).toBe(false); + expect(mockDialogRef.componentInstance.btnOkText).toBe('Yes'); + expect(mockDialogRef.componentInstance.btnCancelText).toBe('No'); + expect(mockDialogRef.disableClose).toBe(true); + }); + + it('should return afterClosed observable', done => { + service.confirmCareContext('T', 'M').subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/global-error-handler.service.spec.ts b/src/app/app-modules/core/services/global-error-handler.service.spec.ts new file mode 100644 index 00000000..2d0b787d --- /dev/null +++ b/src/app/app-modules/core/services/global-error-handler.service.spec.ts @@ -0,0 +1,108 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { ErrorHandler } from '@angular/core'; +import { GlobalErrorHandler } from './global-error-handler.service'; + +describe('GlobalErrorHandler', () => { + let service: GlobalErrorHandler; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [GlobalErrorHandler], + }); + service = TestBed.inject(GlobalErrorHandler); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should implement ErrorHandler', () => { + expect(service instanceof GlobalErrorHandler).toBeTruthy(); + expect(typeof service.handleError).toBe('function'); + }); + + describe('#handleError', () => { + it('should log the error to console', () => { + const consoleSpy = spyOn(console, 'log'); + const testError = new Error('Test error'); + + try { + service.handleError(testError); + } catch (e) { + // expected to throw + } + + expect(consoleSpy).toHaveBeenCalledWith(testError); + }); + + it('should re-throw the error after logging', () => { + spyOn(console, 'log'); + const testError = new Error('Test error message'); + + expect(() => service.handleError(testError)).toThrowError( + 'Test error message' + ); + }); + + it('should handle errors with different messages', () => { + spyOn(console, 'log'); + const testError = new Error('Another error'); + + expect(() => service.handleError(testError)).toThrowError( + 'Another error' + ); + }); + + it('should log and re-throw TypeError', () => { + const consoleSpy = spyOn(console, 'log'); + const typeError = new TypeError('Type mismatch'); + + expect(() => service.handleError(typeError)).toThrow(); + expect(consoleSpy).toHaveBeenCalledWith(typeError); + }); + + it('should log and re-throw RangeError', () => { + const consoleSpy = spyOn(console, 'log'); + const rangeError = new RangeError('Out of range'); + + expect(() => service.handleError(rangeError)).toThrow(); + expect(consoleSpy).toHaveBeenCalledWith(rangeError); + }); + + it('should log the exact error object passed in', () => { + const consoleSpy = spyOn(console, 'log'); + const error = new Error('specific'); + + try { + service.handleError(error); + } catch (e) { + // expected + } + + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy.calls.first().args[0]).toBe(error); + }); + }); +}); diff --git a/src/app/app-modules/core/services/http-interceptor.service.spec.ts b/src/app/app-modules/core/services/http-interceptor.service.spec.ts new file mode 100644 index 00000000..7754405d --- /dev/null +++ b/src/app/app-modules/core/services/http-interceptor.service.spec.ts @@ -0,0 +1,685 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { + HttpClient, + HttpRequest, + HttpHandler, + HttpResponse, + HttpErrorResponse, + HttpHeaders, + HTTP_INTERCEPTORS, + HttpEvent, +} from '@angular/common/http'; +import { Router, NavigationEnd } from '@angular/router'; +import { Subject, of, throwError, EMPTY } from 'rxjs'; +import { HttpInterceptorService } from './http-interceptor.service'; +import { SpinnerService } from './spinner.service'; +import { ConfirmationService } from './confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { CookieService } from 'ngx-cookie-service'; +import { environment } from 'src/environments/environment'; + +describe('HttpInterceptorService', () => { + let service: HttpInterceptorService; + let httpMock: HttpTestingController; + let httpClient: HttpClient; + let spinnerServiceSpy: jasmine.SpyObj; + let confirmationServiceSpy: any; + let sessionStorageSpy: jasmine.SpyObj; + let cookieServiceSpy: jasmine.SpyObj; + let routerEvents$: Subject; + let routerSpy: any; + + beforeEach(() => { + routerEvents$ = new Subject(); + + spinnerServiceSpy = jasmine.createSpyObj('SpinnerService', ['setLoading']); + confirmationServiceSpy = { + alert: jasmine.createSpy('alert').and.returnValue({ + afterClosed: () => of(null), + }), + dialog: { + closeAll: jasmine.createSpy('closeAll'), + }, + }; + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'clear', + ]); + cookieServiceSpy = jasmine.createSpyObj('CookieService', [ + 'get', + 'set', + 'delete', + ]); + routerSpy = { + events: routerEvents$.asObservable(), + navigate: jasmine + .createSpy('navigate') + .and.returnValue(Promise.resolve(true)), + }; + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: HTTP_INTERCEPTORS, + useClass: HttpInterceptorService, + multi: true, + }, + { provide: SpinnerService, useValue: spinnerServiceSpy }, + { provide: ConfirmationService, useValue: confirmationServiceSpy }, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + { provide: CookieService, useValue: cookieServiceSpy }, + { provide: Router, useValue: routerSpy }, + ], + }); + + httpClient = TestBed.inject(HttpClient); + httpMock = TestBed.inject(HttpTestingController); + service = TestBed.inject(HTTP_INTERCEPTORS).find( + i => i instanceof HttpInterceptorService + ) as HttpInterceptorService; + }); + + afterEach(() => { + // Drop any real 27-minute session timer started by a successful request. + (service as any).clearSessionTimeoutTimer(); + httpMock.verify(); + sessionStorage.clear(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('isSessionExpiryInProgress', () => { + it('should return false initially', () => { + expect(service.isSessionExpiryInProgress()).toBe(false); + }); + }); + + describe('intercept - header injection', () => { + it('should add Authorization and Content-Type headers for regular JSON requests', () => { + sessionStorage.setItem('key', 'test-auth-key'); + sessionStorageSpy.getItem.and.returnValue('test-server-key'); + + httpClient.post('/api/test', { data: 1 }).subscribe(); + + const req = httpMock.expectOne('/api/test'); + expect(req.request.headers.get('Authorization')).toBe('test-auth-key'); + expect(req.request.headers.get('Content-Type')).toBe('application/json'); + expect(req.request.headers.get('ServerAuthorization')).toBe( + 'test-server-key' + ); + req.flush({}); + }); + + it('should use empty string for Authorization when key is not in sessionStorage', () => { + sessionStorageSpy.getItem.and.returnValue(null); + + httpClient.post('/api/test', { data: 1 }).subscribe(); + + const req = httpMock.expectOne('/api/test'); + expect(req.request.headers.get('Authorization')).toBe(''); + req.flush({}); + }); + + it('should remove Authorization and set Content-Type for platform-feedback requests', () => { + httpClient.post('/api/platform-feedback', { data: 1 }).subscribe(); + + const req = httpMock.expectOne('/api/platform-feedback'); + expect(req.request.headers.has('Authorization')).toBe(false); + expect(req.request.headers.get('Content-Type')).toBe('application/json'); + req.flush({}); + }); + + it('should set Authorization but not Content-Type for FormData requests', () => { + sessionStorage.setItem('key', 'form-auth'); + sessionStorageSpy.getItem.and.returnValue(null); + + const formData = new FormData(); + formData.append('file', new Blob(['test']), 'test.txt'); + + httpClient.post('/api/upload', formData).subscribe(); + + const req = httpMock.expectOne('/api/upload'); + expect(req.request.headers.get('Authorization')).toBe('form-auth'); + // FormData should NOT have Content-Type manually set + // (browser sets multipart/form-data automatically) + req.flush({}); + }); + }); + + describe('intercept - spinner behavior', () => { + it('should call setLoading(true) for non-cti requests', () => { + httpClient.get('/api/data').subscribe(); + + const req = httpMock.expectOne('/api/data'); + req.flush({ data: 'test' }); + + expect(spinnerServiceSpy.setLoading).toHaveBeenCalledWith(true); + }); + + it('should not call setLoading(true) for cti/getAgentState requests', () => { + httpClient.get('/cti/getAgentState').subscribe(); + + const req = httpMock.expectOne('/cti/getAgentState'); + req.flush({ data: 'test' }); + + // setLoading(true) should not be called for cti requests + // but setLoading(false) is called in finalize + const trueCallCount = spinnerServiceSpy.setLoading.calls + .allArgs() + .filter(args => args[0] === true).length; + expect(trueCallCount).toBe(0); + }); + + it('should call setLoading(false) in finalize when pendingRequests reaches 0', () => { + httpClient.get('/api/data').subscribe(); + + const req = httpMock.expectOne('/api/data'); + req.flush({}); + + expect(spinnerServiceSpy.setLoading).toHaveBeenCalledWith(false); + }); + }); + + describe('intercept - error handling', () => { + it('should call setLoading(false) on error', () => { + httpClient.get('/api/fail').subscribe({ + error: () => {}, + }); + + const req = httpMock.expectOne('/api/fail'); + req.error(new ProgressEvent('error'), { + status: 500, + statusText: 'Server Error', + }); + + expect(spinnerServiceSpy.setLoading).toHaveBeenCalledWith(false); + }); + + it('should handle 401 errors by triggering session expiry', fakeAsync(() => { + httpClient.get('/api/protected').subscribe({ + next: () => {}, + error: () => {}, + complete: () => {}, + }); + + const req = httpMock.expectOne('/api/protected'); + req.error(new ProgressEvent('error'), { + status: 401, + statusText: 'Unauthorized', + }); + + tick(); + expect(service.isSessionExpiryInProgress()).toBe(true); + })); + + it('should propagate non-session errors to the subscriber', done => { + httpClient.get('/api/fail').subscribe({ + error: (err: HttpErrorResponse) => { + expect(err.status).toBe(500); + done(); + }, + }); + + const req = httpMock.expectOne('/api/fail'); + req.error(new ProgressEvent('error'), { + status: 500, + statusText: 'Server Error', + }); + }); + }); + + describe('router events subscription', () => { + it('should reset session state when navigating to /login', () => { + // Simulate a 401 to set isHandlingSessionExpiry to true + // Then emit a login navigation event + routerEvents$.next({ url: '/login' }); + + expect(service.isSessionExpiryInProgress()).toBe(false); + }); + + it('should not reset session state for non-login routes', fakeAsync(() => { + // First trigger a 401 to set session expiry in progress + httpClient.get('/api/test').subscribe({ + error: () => {}, + complete: () => {}, + }); + + const req = httpMock.expectOne('/api/test'); + req.error(new ProgressEvent('error'), { + status: 401, + statusText: 'Unauthorized', + }); + tick(); + + expect(service.isSessionExpiryInProgress()).toBe(true); + + // Navigate to a non-login route + routerEvents$.next({ url: '/dashboard' }); + + // Should still be handling session expiry + expect(service.isSessionExpiryInProgress()).toBe(true); + })); + }); + + describe('intercept - successful response handling', () => { + it('should return the response body on success', done => { + httpClient.get('/api/data').subscribe((res: any) => { + expect(res).toEqual({ data: 'value' }); + done(); + }); + + const req = httpMock.expectOne('/api/data'); + req.flush({ data: 'value' }); + }); + + it('should reset session timeout timer for authenticated requests', () => { + sessionStorage.setItem('authenticationToken', 'token123'); + + httpClient.get('/api/data').subscribe(); + + const req = httpMock.expectOne('/api/data'); + req.flush({ data: 'ok' }); + + // Timer should be set (we cannot directly check private properties, + // but we can verify no error was thrown) + expect(spinnerServiceSpy.setLoading).toHaveBeenCalled(); + }); + + it('should not restart timer for login endpoint responses', () => { + sessionStorage.setItem('authenticationToken', 'token123'); + + httpClient.post('/user/userAuthenticate', {}).subscribe(); + + const req = httpMock.expectOne('/user/userAuthenticate'); + req.flush({ statusCode: 200 }); + + // No error means the exclusion worked correctly + expect(spinnerServiceSpy.setLoading).toHaveBeenCalled(); + }); + + it('should not restart timer for platform-feedback endpoint', () => { + sessionStorage.setItem('authenticationToken', 'token123'); + + httpClient.post('/platform-feedback', {}).subscribe(); + + const req = httpMock.expectOne('/platform-feedback'); + req.flush({ statusCode: 200 }); + + expect(spinnerServiceSpy.setLoading).toHaveBeenCalled(); + }); + + it('should not restart timer when authenticationToken is absent', () => { + sessionStorage.removeItem('authenticationToken'); + + httpClient.get('/api/data').subscribe(); + + const req = httpMock.expectOne('/api/data'); + req.flush({ data: 'ok' }); + + expect(spinnerServiceSpy.setLoading).toHaveBeenCalled(); + }); + }); + + describe('intercept - multiple concurrent requests', () => { + it('should track pending requests and hide spinner only when all complete', () => { + httpClient.get('/api/one').subscribe(); + httpClient.get('/api/two').subscribe(); + + const req1 = httpMock.expectOne('/api/one'); + const req2 = httpMock.expectOne('/api/two'); + + // Complete first request + req1.flush({}); + + // Spinner should still be loading since req2 is pending + // (The last setLoading call might still be true because pendingRequests > 0) + + // Complete second request + req2.flush({}); + + // Now spinner should be hidden + const lastCall = spinnerServiceSpy.setLoading.calls.mostRecent().args[0]; + expect(lastCall).toBe(false); + }); + }); + + describe('intercept - login request session reset', () => { + it('should reset session state on successful login response', fakeAsync(() => { + httpClient + .post('/user/userAuthenticate', { userName: 'test', password: 'pass' }) + .subscribe(); + + const req = httpMock.expectOne('/user/userAuthenticate'); + req.flush({ statusCode: 200, data: { token: 'abc' } }); + + tick(); + + // After a successful login, session expiry flag should be false + expect(service.isSessionExpiryInProgress()).toBe(false); + })); + }); + describe('intercept - session expiry responses', () => { + beforeEach(() => spyOn(console, 'error')); + + it('swallows a 401, closes dialogs, alerts and navigates to login', fakeAsync(() => { + const next = jasmine.createSpy('next'); + const error = jasmine.createSpy('error'); + const complete = jasmine.createSpy('complete'); + httpClient.get('/api/protected').subscribe({ next, error, complete }); + httpMock + .expectOne('/api/protected') + .flush({}, { status: 401, statusText: 'Unauthorized' }); + tick(); + expect(error).not.toHaveBeenCalled(); + expect(complete).toHaveBeenCalled(); + expect(confirmationServiceSpy.dialog.closeAll).toHaveBeenCalled(); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith( + 'Session has expired. Please login again.', + 'error' + ); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']); + expect(spinnerServiceSpy.setLoading).toHaveBeenCalledWith(false); + })); + + it('treats a 200 error carrying statusCode 5002 as session expiry', fakeAsync(() => { + const error = jasmine.createSpy('error'); + httpClient.get('/api/x').subscribe({ error }); + httpMock + .expectOne('/api/x') + .flush( + { statusCode: 5002, errorMessage: 'Token invalid' }, + { status: 200, statusText: 'OK' } + ); + tick(); + // A 200 is not an error for HttpClient, so nothing is intercepted here. + expect(error).not.toHaveBeenCalled(); + expect(service.isSessionExpiryInProgress()).toBeFalse(); + })); + + it('handles a 5002 error event with status 200', fakeAsync(() => { + const handler = { + handle: () => + throwError( + () => + new HttpErrorResponse({ + status: 200, + error: { statusCode: 5002 }, + }) + ), + } as HttpHandler; + const complete = jasmine.createSpy('complete'); + const error = jasmine.createSpy('error'); + service + .intercept(new HttpRequest('GET', '/api/x'), handler) + .subscribe({ error, complete }); + tick(); + expect(complete).toHaveBeenCalled(); + expect(error).not.toHaveBeenCalled(); + expect(service.isSessionExpiryInProgress()).toBeTrue(); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']); + })); + + it('propagates a 401 when expiry is already being handled', fakeAsync(() => { + (service as any).isHandlingSessionExpiry = true; + const error = jasmine.createSpy('error'); + httpClient.get('/api/again').subscribe({ error }); + httpMock + .expectOne('/api/again') + .flush({}, { status: 401, statusText: 'Unauthorized' }); + tick(); + expect(error).toHaveBeenCalled(); + expect(error.calls.mostRecent().args[0].status).toBe(401); + expect(routerSpy.navigate).not.toHaveBeenCalled(); + })); + + it('does not reset state for login responses (lower-cased URL never matches "userAuthenticate")', () => { + (service as any).isHandlingSessionExpiry = true; + httpClient.post('/user/userAuthenticate', {}).subscribe(); + httpMock.expectOne('/user/userAuthenticate').flush({}); + expect(service.isSessionExpiryInProgress()).toBeTrue(); + }); + + it('resets state on a /login router event', () => { + (service as any).isHandlingSessionExpiry = true; + (service as any).logoutMessageShown = true; + routerEvents$.next({ url: '/login' }); + expect(service.isSessionExpiryInProgress()).toBeFalse(); + expect((service as any).logoutMessageShown).toBeFalse(); + }); + }); + + describe('getErrorMessage', () => { + const fallback = 'Your session has expired. Please login again.'; + const msg = (e: any) => (service as any).getErrorMessage(e); + + it('returns non-blank strings and falls back for blank ones', () => { + expect(msg('Expired!')).toBe('Expired!'); + expect(msg(' ')).toBe(fallback); + expect(msg('')).toBe(fallback); + }); + + it('reads message, errorMessage or error string properties', () => { + expect(msg({ message: 'm' })).toBe('m'); + expect(msg({ errorMessage: 'em' })).toBe('em'); + expect(msg({ error: 'e' })).toBe('e'); + expect(msg({ message: 1, errorMessage: 2, error: 3 })).toBe(fallback); + }); + + it('falls back for null and other types', () => { + expect(msg(null)).toBe(fallback); + expect(msg(42)).toBe(fallback); + }); + + it('falls back when reading the error throws', () => { + spyOn(console, 'error'); + const bad = { + get message() { + throw new Error('boom'); + }, + }; + expect(msg(bad)).toBe(fallback); + expect(console.error).toHaveBeenCalled(); + }); + }); + + describe('handleSessionExpiry (unlocked)', () => { + const run = (m: any) => (service as any).handleSessionExpiry(m); + + beforeEach(() => spyOn(console, 'error')); + + it('clears storage, navigates and shows the message once after 300ms', fakeAsync(() => { + sessionStorage.setItem('key', 'k'); + run('Gone'); + expect(sessionStorage.getItem('key')).toBeNull(); + expect(sessionStorageSpy.clear).toHaveBeenCalled(); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']); + expect(service.isSessionExpiryInProgress()).toBeTrue(); + tick(299); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + tick(1); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith( + 'Gone', + 'error' + ); + + // a second unlocked expiry does not show the dialog again + confirmationServiceSpy.alert.calls.reset(); + (service as any).isHandlingSessionExpiry = false; + run('Again'); + tick(300); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + })); + + it('logs when navigation is refused and still shows the dialog', fakeAsync(() => { + routerSpy.navigate.and.returnValue(Promise.resolve(false)); + run('x'); + tick(300); + expect(console.error).toHaveBeenCalledWith('Navigation to login failed'); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith('x', 'error'); + })); + + it('logs dialog errors from afterClosed', fakeAsync(() => { + confirmationServiceSpy.alert.and.returnValue({ + afterClosed: () => throwError(() => 'dlg'), + }); + run('x'); + tick(300); + expect(console.error).toHaveBeenCalledWith('Error in dialog:', 'dlg'); + })); + + it('logs when opening the dialog throws', fakeAsync(() => { + confirmationServiceSpy.alert.and.throwError('no dialog'); + run('x'); + tick(300); + expect(console.error).toHaveBeenCalledWith( + 'Failed to show session expiry dialog:', + jasmine.any(Error) + ); + })); + + it('logs a rejected navigation', fakeAsync(() => { + routerSpy.navigate.and.returnValue(Promise.reject('nav')); + run('x'); + tick(); + expect(console.error).toHaveBeenCalledWith('Navigation error:', 'nav'); + })); + + it('logs a synchronous navigation failure', () => { + routerSpy.navigate.and.throwError('sync'); + run('x'); + expect(console.error).toHaveBeenCalledWith( + 'Error during session expiry handling:', + jasmine.any(Error) + ); + }); + }); + + describe('session timeout warning', () => { + const TIMEOUT = 27 * 60 * 1000; + + function authedRequest() { + sessionStorage.setItem('authenticationToken', 't'); + sessionStorage.setItem('isAuthenticated', 'true'); + httpClient.get('/api/data').subscribe(); + httpMock.expectOne('/api/data').flush({}); + } + + function warnWith(result: any) { + confirmationServiceSpy.alert.and.returnValue({ + afterClosed: () => of(result), + }); + } + + it('warns 27 minutes after an authenticated response', fakeAsync(() => { + warnWith(null); + authedRequest(); + tick(TIMEOUT - 1); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + tick(1); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith( + 'Your session is about to expire. Do you need more time?', + 'sessionTimeOut' + ); + expect(routerSpy.navigate).not.toHaveBeenCalled(); + })); + + it('uses the language set text and extends the session on continue', fakeAsync(() => { + service.currentLanguageSet = { sessionTimeoutWarning: 'More time?' }; + warnWith({ action: 'continue' }); + authedRequest(); + tick(TIMEOUT); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith( + 'More time?', + 'sessionTimeOut' + ); + const req = httpMock.expectOne(environment.extendSessionUrl); + expect(req.request.method).toBe('POST'); + req.flush({}); + expect((service as any).sessionTimeoutRef).toBeTruthy(); + (service as any).clearSessionTimeoutTimer(); + })); + + it('restarts the timer even when extending fails', fakeAsync(() => { + spyOn(console, 'error'); + warnWith({ action: 'continue' }); + authedRequest(); + tick(TIMEOUT); + httpMock + .expectOne(environment.extendSessionUrl) + .flush({}, { status: 500, statusText: 'err' }); + expect(console.error).toHaveBeenCalled(); + expect((service as any).sessionTimeoutRef).toBeTruthy(); + (service as any).clearSessionTimeoutTimer(); + })); + + it('expires the session on timeout using the language text', fakeAsync(() => { + service.currentLanguageSet = { sessionExpired: 'Bye' }; + warnWith({ action: 'timeout' }); + authedRequest(); + tick(TIMEOUT); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']); + expect(service.isSessionExpiryInProgress()).toBeTrue(); + tick(300); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith('Bye', 'error'); + })); + + it('expires the session on cancel with the default text', fakeAsync(() => { + warnWith({ action: 'cancel' }); + authedRequest(); + tick(TIMEOUT); + tick(300); + expect(confirmationServiceSpy.alert).toHaveBeenCalledWith( + 'Your session has expired. Please login again.', + 'error' + ); + })); + + it('skips the warning when no longer authenticated', fakeAsync(() => { + authedRequest(); + sessionStorage.removeItem('isAuthenticated'); + tick(TIMEOUT); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + })); + + it('skips the warning while expiry is being handled', fakeAsync(() => { + authedRequest(); + (service as any).isHandlingSessionExpiry = true; + tick(TIMEOUT); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + })); + + it('skips the warning when the token is gone', () => { + (service as any).showSessionExpiryWarning(); + expect(confirmationServiceSpy.alert).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/app-modules/core/services/http-service.service.spec.ts b/src/app/app-modules/core/services/http-service.service.spec.ts new file mode 100644 index 00000000..f9aae2c0 --- /dev/null +++ b/src/app/app-modules/core/services/http-service.service.spec.ts @@ -0,0 +1,162 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; +import { HttpServiceService } from './http-service.service'; +import { environment } from 'src/environments/environment'; + +describe('HttpServiceService', () => { + let service: HttpServiceService; + let httpMock: HttpTestingController; + + beforeEach(() => { + localStorage.removeItem('appLanguage'); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [HttpServiceService], + }); + + service = TestBed.inject(HttpServiceService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + localStorage.removeItem('appLanguage'); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('constructor', () => { + it('should initialize language as null when localStorage has no appLanguage', () => { + expect(service.language).toBeNull(); + }); + + it('should initialize language from localStorage when available', () => { + const langData = { english: 'Hello' }; + localStorage.setItem('appLanguage', JSON.stringify(langData)); + const http = TestBed.inject(HttpClient); + const fresh = new HttpServiceService(http, http); + expect(fresh.language).toEqual(langData); + }); + }); + + describe('listen and filter', () => { + it('should emit values through the _listners Subject', done => { + service.listen().subscribe(value => { + expect(value).toBe('testFilter'); + done(); + }); + + service.filter('testFilter'); + }); + + it('should emit multiple filter values in order', () => { + const received: string[] = []; + service.listen().subscribe(value => { + received.push(value); + }); + + service.filter('first'); + service.filter('second'); + service.filter('third'); + + expect(received).toEqual(['first', 'second', 'third']); + }); + }); + + describe('fetchLanguageSet', () => { + it('should make a GET request to the language list URL', () => { + service.fetchLanguageSet().subscribe((res: any) => { + expect(res).toEqual({ data: ['en', 'hi'] }); + }); + + const req = httpMock.expectOne(environment.getLanguageList); + expect(req.request.method).toBe('GET'); + req.flush({ data: ['en', 'hi'] }); + }); + }); + + describe('getLanguage', () => { + it('should make a GET request to the provided URL', () => { + const url = 'https://example.com/language/en.json'; + + service.getLanguage(url).subscribe((res: any) => { + expect(res).toEqual({ greeting: 'Hello' }); + }); + + const req = httpMock.expectOne(url); + expect(req.request.method).toBe('GET'); + req.flush({ greeting: 'Hello' }); + }); + }); + + describe('getCurrentLanguage', () => { + it('should update the language property', () => { + const langData = { greeting: 'Namaste' }; + service.getCurrentLanguage(langData); + expect(service.language).toEqual(langData); + }); + + it('should save language to localStorage', () => { + const langData = { greeting: 'Hello' }; + service.getCurrentLanguage(langData); + expect(localStorage.getItem('appLanguage')).toBe( + JSON.stringify(langData) + ); + }); + + it('should emit the new language on appCurrentLanguge BehaviorSubject', done => { + const langData = { greeting: 'Hola' }; + + // Skip the initial emission (which is null or stored value) + let emissionCount = 0; + service.currentLangugae$.subscribe(lang => { + emissionCount++; + if (emissionCount === 2) { + expect(lang).toEqual(langData); + done(); + } + }); + + service.getCurrentLanguage(langData); + }); + + it('should make the language available via currentLangugae$ observable', done => { + const langData = { key: 'value' }; + service.getCurrentLanguage(langData); + + service.currentLangugae$.subscribe(lang => { + expect(lang).toEqual(langData); + done(); + }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/inventory.service.spec.ts b/src/app/app-modules/core/services/inventory.service.spec.ts new file mode 100644 index 00000000..d5034af3 --- /dev/null +++ b/src/app/app-modules/core/services/inventory.service.spec.ts @@ -0,0 +1,274 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { InventoryService } from './inventory.service'; +import { ConfirmationService } from './confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; + +describe('InventoryService', () => { + let service: InventoryService; + let mockConfirmationService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + let mockDocument: any; + + beforeEach(() => { + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + mockDocument = { + location: { + protocol: 'https:', + host: 'localhost:4202', + pathname: '/mmuui-v1.0/', + }, + }; + + // Constructed directly: overriding DOCUMENT in TestBed breaks Angular's + // own DOM services (e.g. this._doc.querySelectorAll is not a function). + service = new InventoryService( + mockDocument, + mockConfirmationService, + mockSessionStorageService + ); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#getParentAPI', () => { + it('should return environment.parentAPI', () => { + expect(service.getParentAPI()).toBe(environment.parentAPI); + }); + }); + + describe('#getAuthKey', () => { + it('should return key when isAuthenticated is set', () => { + spyOn(sessionStorage, 'getItem').and.callFake((key: string) => { + if (key === 'isAuthenticated') return 'true'; + if (key === 'key') return 'authKey123'; + return null; + }); + expect(service.getAuthKey()).toBe('authKey123'); + }); + + it('should return undefined when isAuthenticated is not set', () => { + spyOn(sessionStorage, 'getItem').and.returnValue(null); + expect(service.getAuthKey()).toBeUndefined(); + }); + + it('should return undefined when isAuthenticated is falsy', () => { + spyOn(sessionStorage, 'getItem').and.returnValue(null); + expect(service.getAuthKey()).toBeUndefined(); + }); + }); + + describe('#getFacilityID', () => { + it('should return facilityID when it exists in session storage', () => { + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'facilityID') return 'facility123'; + return null; + }); + expect(service.getFacilityID()).toBe('facility123'); + }); + + it('should return undefined when facilityID is not in session storage', () => { + mockSessionStorageService.getItem.and.returnValue(null); + expect(service.getFacilityID()).toBeUndefined(); + }); + }); + + describe('#getProtocol', () => { + it('should return the document location protocol', () => { + expect(service.getProtocol()).toBe('https:'); + }); + }); + + describe('#getHost', () => { + it('should return host + pathname', () => { + expect(service.getHost()).toBe('localhost:4202/mmuui-v1.0/'); + }); + }); + + describe('#getVanID', () => { + it('should parse serviceLineDetails and return vanID', () => { + const serviceLineDetails = JSON.stringify({ + vanID: 42, + parkingPlaceID: 7, + }); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return serviceLineDetails; + return null; + }); + expect(service.getVanID()).toBe(42); + }); + }); + + describe('#getppID', () => { + it('should parse serviceLineDetails and return parkingPlaceID', () => { + const serviceLineDetails = JSON.stringify({ + vanID: 42, + parkingPlaceID: 7, + }); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return serviceLineDetails; + return null; + }); + expect(service.getppID()).toBe(7); + }); + }); + + describe('#getServiceDetails', () => { + it('should return serviceName from session storage', () => { + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'serviceName') return 'MMU'; + return null; + }); + expect(service.getServiceDetails()).toBe('MMU'); + }); + + it('should return null when serviceName is not set', () => { + mockSessionStorageService.getItem.and.returnValue(null); + expect(service.getServiceDetails()).toBeNull(); + }); + }); + + describe('#moveToInventory', () => { + let originalHref: string; + + beforeEach(() => { + originalHref = window.location.href; + }); + + it('should call confirmationService.alert when authKey is missing', () => { + spyOn(sessionStorage, 'getItem').and.returnValue(null); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'facilityID') return 'facility123'; + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); + if (key === 'serviceName') return 'MMU'; + return null; + }); + + service.moveToInventory('ben1', 'visit1', 'flow1', 'en', 'reg1'); + + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No Facility mapped, Can not connect to Inventory', + 'error' + ); + }); + + it('should call confirmationService.alert when facilityID is missing', () => { + spyOn(sessionStorage, 'getItem').and.callFake((key: string) => { + if (key === 'isAuthenticated') return 'true'; + if (key === 'key') return 'authKey123'; + return null; + }); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'facilityID') return null; + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); + if (key === 'serviceName') return 'MMU'; + return null; + }); + + service.moveToInventory('ben1', 'visit1', 'flow1', 'en', 'reg1'); + + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No Facility mapped, Can not connect to Inventory', + 'error' + ); + }); + + it('should construct inventory URL and set window.location.href when all data present', () => { + spyOn(sessionStorage, 'getItem').and.callFake((key: string) => { + if (key === 'isAuthenticated') return 'true'; + if (key === 'key') return 'authKey123'; + return null; + }); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'facilityID') return 'facility123'; + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); + if (key === 'serviceName') return 'MMU'; + return null; + }); + + // Point the inventory URL at an in-page fragment so the real + // window.location.href assignment cannot reload the Karma page. + const originalUrl = environment.INVENTORY_URL; + // Absolute same-document URL (a bare '#...' would resolve against ). + (environment as any).INVENTORY_URL = + window.location.href.split('#')[0] + '#inventory-test?'; + try { + service.moveToInventory('ben1', 'visit1', 'flow1', 'en', 'reg1'); + + expect(service.inventoryUrl).toContain('#inventory-test?'); + expect(service.inventoryUrl).toContain('protocol=https:'); + expect(service.inventoryUrl).toContain('user=authKey123'); + expect(service.inventoryUrl).toContain('facility=facility123'); + expect(service.inventoryUrl).toContain('ben=ben1'); + expect(service.inventoryUrl).toContain('visit=visit1'); + expect(service.inventoryUrl).toContain('flow=flow1'); + expect(service.inventoryUrl).toContain('reg=reg1'); + expect(service.inventoryUrl).toContain('vanID=1'); + expect(service.inventoryUrl).toContain('ppID=2'); + expect(service.inventoryUrl).toContain('serviceName=MMU'); + expect(service.inventoryUrl).toContain('currentLanguage=en'); + expect(window.location.hash).toContain('#inventory-test'); + expect(mockConfirmationService.alert).not.toHaveBeenCalled(); + } finally { + (environment as any).INVENTORY_URL = originalUrl; + history.replaceState(null, '', originalHref); + } + }); + + it('should show error alert when protocol is missing', () => { + spyOn(sessionStorage, 'getItem').and.callFake((key: string) => { + if (key === 'isAuthenticated') return 'true'; + if (key === 'key') return 'authKey123'; + return null; + }); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'facilityID') return 'facility123'; + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); + if (key === 'serviceName') return 'MMU'; + return null; + }); + + // Make protocol falsy + mockDocument.location.protocol = ''; + + service.moveToInventory('ben1', 'visit1', 'flow1', 'en', 'reg1'); + + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No Facility mapped, Can not connect to Inventory', + 'error' + ); + }); + }); +}); diff --git a/src/app/app-modules/core/services/iot.service.spec.ts b/src/app/app-modules/core/services/iot.service.spec.ts new file mode 100644 index 00000000..32ee5274 --- /dev/null +++ b/src/app/app-modules/core/services/iot.service.spec.ts @@ -0,0 +1,298 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { IotService } from './iot.service'; +import { environment } from 'src/environments/environment'; + +describe('IotService', () => { + let service: IotService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [IotService], + }); + service = TestBed.inject(IotService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('URL properties', () => { + it('should set baseurl from environment', () => { + expect(service.baseurl).toBe(environment.ioturl); + }); + + it('should set deviceStatusurl from environment', () => { + expect(service.deviceStatusurl).toBe(environment.deviceStatusurl); + }); + + it('should set deviceBluetoothurl from environment', () => { + expect(service.deviceBluetoothurl).toBe(environment.deviceBluetoothurl); + }); + + it('should set connectdeviceBluetoothurl from environment', () => { + expect(service.connectdeviceBluetoothurl).toBe( + environment.connectdeviceBluetoothurl + ); + }); + + it('should set disconnectdeviceBluetoothurl from environment', () => { + expect(service.disconnectdeviceBluetoothurl).toBe( + environment.deviceDisconnectUrl + ); + }); + }); + + describe('BehaviorSubject - disconnectValue', () => { + it('should have initial disconnect value as false', () => { + expect(service.disconnect).toBe(false); + }); + + it('should have disconnectValue$ observable', () => { + expect(service.disconnectValue$).toBeDefined(); + }); + + it('should emit initial value from disconnectValue$', (done: DoneFn) => { + service.disconnectValue$.subscribe(val => { + expect(val).toBe(false); + done(); + }); + }); + }); + + describe('#setBluetoothConnected', () => { + it('should update disconnect property', () => { + service.setBluetoothConnected(true); + expect(service.disconnect).toBe(true); + }); + + it('should emit the new value through disconnectValue$', (done: DoneFn) => { + let callCount = 0; + service.disconnectValue$.subscribe(val => { + callCount++; + if (callCount === 2) { + expect(val).toBe(true); + done(); + } + }); + service.setBluetoothConnected(true); + }); + + it('should update with false value', () => { + service.setBluetoothConnected(true); + service.setBluetoothConnected(false); + expect(service.disconnect).toBe(false); + }); + + it('should accept any value type', (done: DoneFn) => { + let callCount = 0; + service.disconnectValue$.subscribe(val => { + callCount++; + if (callCount === 2) { + expect(val).toBe('connected' as any); + done(); + } + }); + service.setBluetoothConnected('connected'); + }); + }); + + describe('#startAPI', () => { + it('should make a POST request with null body', () => { + const input = '/api/v1/physical_tests/weight'; + const mockResponse = { status: 'started' }; + + service.startAPI(input).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.baseurl + input); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toBeNull(); + req.flush(mockResponse); + }); + }); + + describe('#statusAPI', () => { + it('should make a GET request', () => { + const input = '/api/v1/physical_tests/weight'; + const mockResponse = { status: 'running' }; + + service.statusAPI(input).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.baseurl + input); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + }); + }); + + describe('#endAPI', () => { + it('should make a PUT request with null body', () => { + const input = '/api/v1/physical_tests/weight'; + const mockResponse = { status: 'ended' }; + + service.endAPI(input).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.baseurl + input); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toBeNull(); + req.flush(mockResponse); + }); + }); + + describe('#endCalibrationAPI', () => { + it('should make a PUT request with headers in body', () => { + const input = '/api/v1/calibration/end'; + const mockResponse = { status: 'calibration ended' }; + + service.endCalibrationAPI(input).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.baseurl + input); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ + headers: { 'Content-Type': ['application/json'] }, + }); + req.flush(mockResponse); + }); + }); + + describe('#getDeviceStatus', () => { + it('should make a GET request to deviceStatusurl', () => { + const mockResponse = { connected: true }; + + service.getDeviceStatus().subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.deviceStatusurl); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + }); + }); + + describe('#getBluetoothDevice', () => { + it('should make a GET request to deviceBluetoothurl', () => { + const mockResponse = { devices: ['device1'] }; + + service.getBluetoothDevice().subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.deviceBluetoothurl); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + }); + }); + + describe('#connectBluetoothDevice', () => { + it('should make a POST request with device string in URL', () => { + const deviceStr = 'device123'; + const mockResponse = { status: 'connected' }; + + service.connectBluetoothDevice(deviceStr).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const expectedUrl = service.connectdeviceBluetoothurl + '/' + deviceStr; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush(mockResponse); + }); + }); + + describe('#disconnectBluetoothDevice', () => { + it('should make a POST request with null body', () => { + const mockResponse = { status: 'disconnected' }; + + service.disconnectBluetoothDevice().subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.disconnectdeviceBluetoothurl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toBeNull(); + req.flush(mockResponse); + }); + }); + + describe('#pairExternalDevice', () => { + it('should make a POST request with empty object body', () => { + const url = '/api/v1/pair/device'; + const mockResponse = { paired: true }; + + service.pairExternalDevice(url).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(service.baseurl + url); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush(mockResponse); + }); + }); + + describe('HTTP error handling', () => { + it('should handle error on startAPI', () => { + const input = '/api/v1/test'; + + service.startAPI(input).subscribe({ + error: error => { + expect(error.status).toBe(500); + }, + }); + + const req = httpMock.expectOne(service.baseurl + input); + req.flush('Error', { status: 500, statusText: 'Server Error' }); + }); + + it('should handle error on getDeviceStatus', () => { + service.getDeviceStatus().subscribe({ + error: error => { + expect(error.status).toBe(404); + }, + }); + + const req = httpMock.expectOne(service.deviceStatusurl); + req.flush('Not Found', { status: 404, statusText: 'Not Found' }); + }); + }); +}); diff --git a/src/app/app-modules/core/services/spinner.service.spec.ts b/src/app/app-modules/core/services/spinner.service.spec.ts new file mode 100644 index 00000000..17797905 --- /dev/null +++ b/src/app/app-modules/core/services/spinner.service.spec.ts @@ -0,0 +1,164 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { SpinnerService, SpinnerState } from './spinner.service'; + +describe('SpinnerService', () => { + let service: SpinnerService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SpinnerService], + }); + + service = TestBed.inject(SpinnerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('setLoading / getLoading', () => { + it('should default loading to false', () => { + expect(service.getLoading()).toBe(false); + }); + + it('should set loading to true', () => { + service.setLoading(true); + expect(service.getLoading()).toBe(true); + }); + + it('should set loading to false', () => { + service.setLoading(true); + service.setLoading(false); + expect(service.getLoading()).toBe(false); + }); + }); + + describe('show', () => { + it('should emit {show: true} on first call', done => { + service.spinnerState.subscribe((state: SpinnerState) => { + expect(state.show).toBe(true); + done(); + }); + + service.show(); + }); + + it('should add to temp array on each call', () => { + service.show(); + service.show(); + expect(service.temp.length).toBe(2); + }); + + it('should only emit {show: true} on the first call, not subsequent calls', () => { + const emissions: SpinnerState[] = []; + service.spinnerState.subscribe((state: SpinnerState) => { + emissions.push(state); + }); + + service.show(); + service.show(); + service.show(); + + expect(emissions.length).toBe(1); + expect(emissions[0].show).toBe(true); + }); + }); + + describe('hide', () => { + it('should emit {show: false} when temp becomes empty', done => { + service.show(); + + service.spinnerState.subscribe((state: SpinnerState) => { + if (!state.show) { + expect(state.show).toBe(false); + done(); + } + }); + + service.hide(); + }); + + it('should not emit when temp still has items', () => { + service.show(); + service.show(); + + const emissions: SpinnerState[] = []; + service.spinnerState.subscribe((state: SpinnerState) => { + emissions.push(state); + }); + + service.hide(); + expect(emissions.length).toBe(0); + expect(service.temp.length).toBe(1); + }); + + it('should not throw when called on empty temp array', () => { + expect(() => service.hide()).not.toThrow(); + }); + + it('should emit {show: false} when temp goes from 1 to 0', () => { + service.temp = [true]; + const emissions: SpinnerState[] = []; + service.spinnerState.subscribe((state: SpinnerState) => { + emissions.push(state); + }); + + service.hide(); + expect(emissions.length).toBe(1); + expect(emissions[0].show).toBe(false); + }); + }); + + describe('clear', () => { + it('should reset temp to [false] then pop it, emitting {show: false}', done => { + service.show(); + service.show(); + service.show(); + + service.spinnerState.subscribe((state: SpinnerState) => { + if (!state.show) { + expect(state.show).toBe(false); + expect(service.temp.length).toBe(0); + done(); + } + }); + + service.clear(); + }); + + it('should work when called without any prior show()', () => { + const emissions: SpinnerState[] = []; + service.spinnerState.subscribe((state: SpinnerState) => { + emissions.push(state); + }); + + service.clear(); + + expect(service.temp.length).toBe(0); + expect(emissions.length).toBe(1); + expect(emissions[0].show).toBe(false); + }); + }); +}); diff --git a/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.spec.ts b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.spec.ts new file mode 100644 index 00000000..56efddc0 --- /dev/null +++ b/src/app/app-modules/data-sync/camp-hub-qr-code/camp-hub-qr-code.component.spec.ts @@ -0,0 +1,146 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef } from '@angular/material/dialog'; + +import { CampHubQrCodeComponent } from './camp-hub-qr-code.component'; +import { environment } from 'src/environments/environment'; +import { createDialogRefMock, NO_ERRORS_SCHEMA } from 'src/testing/test-utils'; + +async function waitUntil(cond: () => boolean) { + for (let i = 0; i < 100 && !cond(); i++) { + await new Promise(r => setTimeout(r, 5)); + } +} + +describe('CampHubQrCodeComponent', () => { + let fixture: ComponentFixture; + let component: CampHubQrCodeComponent; + let httpMock: HttpTestingController; + + async function setup(providers: any[] = []) { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ReactiveFormsModule], + declarations: [CampHubQrCodeComponent], + providers, + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + fixture = TestBed.createComponent(CampHubQrCodeComponent); + component = fixture.componentInstance; + httpMock = TestBed.inject(HttpTestingController); + } + + afterEach(() => httpMock.verify()); + + describe('with a dialog ref', () => { + let dialogRef: any; + + beforeEach(async () => { + dialogRef = createDialogRefMock(); + await setup([{ provide: MatDialogRef, useValue: dialogRef }]); + }); + + it('auto-detects the server URL on init and generates a QR code', async () => { + fixture.detectChanges(); + expect(component.isDetecting).toBeTrue(); + const req = httpMock.expectOne(environment.campHubConnectInfoAPI); + expect(req.request.method).toBe('GET'); + req.flush({ ip: '10.0.0.5', port: 8080 }); + + expect(component.isDetecting).toBeFalse(); + expect(component.urlForm.value.campHubUrl).toBe('http://10.0.0.5:8080/'); + expect(component.isGenerating).toBeTrue(); + await waitUntil(() => !component.isGenerating); + expect(component.generatedUrl).toBe('http://10.0.0.5:8080/'); + expect(component.qrDataUrl).toMatch(/^data:image\/png;base64,/); + }); + + it('flags detection failure on HTTP error', () => { + fixture.detectChanges(); + httpMock + .expectOne(environment.campHubConnectInfoAPI) + .flush('x', { status: 500, statusText: 'err' }); + expect(component.detectionFailed).toBeTrue(); + expect(component.isDetecting).toBeFalse(); + expect(component.qrDataUrl).toBeNull(); + }); + + it('close() closes the dialog', () => { + component.close(); + expect(dialogRef.close).toHaveBeenCalled(); + }); + + it('generate() does nothing when the form is invalid', () => { + component.urlForm.controls.campHubUrl.setValue('ftp://bad'); + component.generate(); + expect(component.isGenerating).toBeFalse(); + expect(component.qrDataUrl).toBeNull(); + }); + + it('generate() trims the URL', async () => { + component.urlForm.controls.campHubUrl.setValue('http://1.2.3.4:8080/ '); + component.generate(); + await waitUntil(() => !component.isGenerating); + expect(component.generatedUrl).toBe('http://1.2.3.4:8080/'); + }); + + it('generate() clears the generating flag when QR creation fails', async () => { + component.urlForm.controls.campHubUrl.setValue( + 'http://' + 'a'.repeat(4000) + ); + component.generate(); + await waitUntil(() => !component.isGenerating); + expect(component.isGenerating).toBeFalse(); + expect(component.qrDataUrl).toBeNull(); + expect(component.generatedUrl).toBeNull(); + }); + + it('downloadQR() does nothing without a QR code', () => { + const click = spyOn(HTMLAnchorElement.prototype, 'click'); + component.downloadQR(); + expect(click).not.toHaveBeenCalled(); + }); + + it('downloadQR() clicks a download link for the QR image', () => { + const click = spyOn(HTMLAnchorElement.prototype, 'click'); + component.qrDataUrl = 'data:image/png;base64,AAA'; + component.downloadQR(); + const anchor = click.calls.mostRecent().object as HTMLAnchorElement; + expect(anchor.download).toBe('camp-hub-server-url.png'); + expect(anchor.href).toBe('data:image/png;base64,AAA'); + }); + }); + + describe('without a dialog ref', () => { + beforeEach(async () => setup()); + + it('close() is a no-op', () => { + expect(() => component.close()).not.toThrow(); + }); + }); +}); diff --git a/src/app/app-modules/data-sync/dashboard/dashboard.component.spec.ts b/src/app/app-modules/data-sync/dashboard/dashboard.component.spec.ts index bd7f10ea..f134ea9a 100644 --- a/src/app/app-modules/data-sync/dashboard/dashboard.component.spec.ts +++ b/src/app/app-modules/data-sync/dashboard/dashboard.component.spec.ts @@ -20,27 +20,31 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DashboardComponent } from './dashboard.component'; -describe('DashboardComponent', () => { +describe('data-sync DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [DashboardComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('renders the shell: header with roles, router outlet and footer', () => { expect(component).toBeTruthy(); + const el: HTMLElement = fixture.nativeElement; + const header = el.querySelector('app-header'); + expect(header).not.toBeNull(); + expect(el.querySelector('router-outlet')).not.toBeNull(); + expect(el.querySelector('app-footer')).not.toBeNull(); }); }); diff --git a/src/app/app-modules/data-sync/master-download/master-download.component.spec.ts b/src/app/app-modules/data-sync/master-download/master-download.component.spec.ts index f7d714f7..cfd5c3fe 100644 --- a/src/app/app-modules/data-sync/master-download/master-download.component.spec.ts +++ b/src/app/app-modules/data-sync/master-download/master-download.component.spec.ts @@ -20,27 +20,155 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; import { MasterDownloadComponent } from './master-download.component'; +import { DataSyncService } from '../shared/service/data-sync.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; describe('MasterDownloadComponent', () => { - let component: MasterDownloadComponent; let fixture: ComponentFixture; + let component: MasterDownloadComponent; + let dataSync: any; + let confirmation: any; + let dialogRef: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + dataSync = autoSpy(DataSyncService); + dataSync.getVanDetailsForMasterDownload.and.returnValue( + of({ statusCode: 200, data: { vanID: 5, vehicalNo: 'MH12' } }) + ); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [MasterDownloadComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ + session: { dataSyncProviderServiceMapID: 9 }, + }), + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(MasterDownloadComponent, { + set: { providers: [{ provide: DataSyncService, useValue: dataSync }] }, + }) + .compileComponents(); fixture = TestBed.createComponent(MasterDownloadComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService); + dialogRef = TestBed.inject(MatDialogRef); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('getVanDetails', () => { + it('shows van details when both vanID and vehicle number exist', () => { + expect(component.showVanDetails).toBeTrue(); + expect(component.vanID).toBe(5); + expect(component.vehicalNo).toBe('MH12'); + }); + + it('hides details when vehicle number is missing', () => { + dataSync.getVanDetailsForMasterDownload.and.returnValue( + of({ statusCode: 200, data: { vanID: 5 } }) + ); + component.getVanDetails(); + expect(component.showVanDetails).toBeFalse(); + expect(component.vanID).toBe(5); + }); + + it('hides details on a non-200 response', () => { + dataSync.getVanDetailsForMasterDownload.and.returnValue( + of({ statusCode: 500 }) + ); + component.getVanDetails(); + expect(component.showVanDetails).toBeFalse(); + }); + + it('hides details on error', () => { + dataSync.getVanDetailsForMasterDownload.and.returnValue(throwingObs()); + component.getVanDetails(); + expect(component.showVanDetails).toBeFalse(); + }); + }); + + describe('syncDownloadData', () => { + it('does nothing when not confirmed', () => { + confirmation.confirm.and.returnValue(of(false)); + component.syncDownloadData(); + expect(dataSync.syncDownloadData).not.toHaveBeenCalled(); + }); + + it('alerts on a failed start', () => { + dataSync.syncDownloadData.and.returnValue( + of({ statusCode: 500, errorMessage: 'no' }) + ); + component.syncDownloadData(); + expect(dataSync.syncDownloadData).toHaveBeenCalledWith({ + vanID: 5, + providerServiceMapID: 9, + }); + expect(confirmation.alert).toHaveBeenCalledWith('no', 'error'); + expect(component.showProgressBar).toBeFalse(); + }); + + it('polls progress until complete', fakeAsync(() => { + dataSync.syncDownloadData.and.returnValue(of({ statusCode: 200 })); + dataSync.syncDownloadDataProgress.and.returnValues( + of({ statusCode: 200, data: { percentage: 50 } }), + of({ statusCode: 200, data: { percentage: 100, failedMasters: 'x|' } }) + ); + component.syncDownloadData(); + expect(component.showProgressBar).toBeTrue(); + tick(2000); + expect(component.progressValue).toBe(50); + tick(2000); + expect(component.failedMasterList).toEqual(['x']); + expect(component.showProgressBar).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Master download finished' + ); + tick(4000); + expect(dataSync.syncDownloadDataProgress).toHaveBeenCalledTimes(2); + discardPeriodicTasks(); + })); + }); + + describe('syncDownloadProgressStatus', () => { + it('keeps a non-blank last entry', () => { + dataSync.syncDownloadDataProgress.and.returnValue( + of({ statusCode: 200, data: { percentage: 100, failedMasters: 'a|b' } }) + ); + component.syncDownloadProgressStatus(); + expect(component.failedMasterList).toEqual(['a', 'b']); + }); + + it('stops and alerts on an error response', () => { + component.showProgressBar = true; + dataSync.syncDownloadDataProgress.and.returnValue( + of({ statusCode: 500, errorMessage: 'broken' }) + ); + component.syncDownloadProgressStatus(); + expect(component.showProgressBar).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('broken', 'error'); + }); + }); + + it('closeDialog closes the dialog', () => { + component.closeDialog(); + expect(dialogRef.close).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/data-sync/shared/service/data-sync.service.spec.ts b/src/app/app-modules/data-sync/shared/service/data-sync.service.spec.ts index 0fcabf2b..39718f2b 100644 --- a/src/app/app-modules/data-sync/shared/service/data-sync.service.spec.ts +++ b/src/app/app-modules/data-sync/shared/service/data-sync.service.spec.ts @@ -20,21 +20,184 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { DataSyncService } from './data-sync.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; describe('DataSyncService', () => { + let service: DataSyncService; + let httpMock: HttpTestingController; + let mockSessionStorage: jasmine.SpyObj; + beforeEach(() => { + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + TestBed.configureTestingModule({ - providers: [DataSyncService], + imports: [HttpClientTestingModule], + providers: [ + DataSyncService, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + }); + + service = TestBed.inject(DataSyncService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getDataSYNCGroup', () => { + it('should make GET request to getDataSYNCGroupUrl', () => { + service.getDataSYNCGroup().subscribe(); + const req = httpMock.expectOne(environment.getDataSYNCGroupUrl); + expect(req.request.method).toBe('GET'); + req.flush([]); }); }); - it('should be created', inject( - [DataSyncService], - (service: DataSyncService) => { - expect(service).toBeTruthy(); - } - )); + describe('dataSyncLogin', () => { + it('should make POST request with credentials', () => { + service.dataSyncLogin('admin', 'pass123', false).subscribe(); + const req = httpMock.expectOne(environment.syncLoginUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + userName: 'admin', + password: 'pass123', + doLogout: false, + }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('userlogoutPreviousSession', () => { + it('should POST userName to logout URL', () => { + service.userlogoutPreviousSession('testUser').subscribe(); + const req = httpMock.expectOne( + environment.syncUserlogoutPreviousSessionUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ userName: 'testUser' }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('syncUploadData', () => { + it('should POST upload request with user and vanID', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'userName') return 'testUser'; + if (key === 'serviceLineDetails') return JSON.stringify({ vanID: 10 }); + return null; + }); + + service.syncUploadData().subscribe(); + const req = httpMock.expectOne(environment.syncDataUploadUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ user: 'testUser', vanID: 10 }); + req.flush({ statusCode: 200 }); + }); + + it('should handle missing serviceLineDetails gracefully', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'userName') return 'testUser'; + if (key === 'serviceLineDetails') return null; + return null; + }); + + service.syncUploadData().subscribe(); + const req = httpMock.expectOne(environment.syncDataUploadUrl); + expect(req.request.body.user).toBe('testUser'); + req.flush({ statusCode: 200 }); + }); + }); + + describe('syncAllGroups', () => { + it('should POST to syncDataUploadUrl with user and vanID', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'userName') return 'user1'; + if (key === 'serviceLineDetails') return JSON.stringify({ vanID: 5 }); + return null; + }); + + service.syncAllGroups().subscribe(); + const req = httpMock.expectOne(environment.syncDataUploadUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ user: 'user1', vanID: 5 }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('syncDownloadData', () => { + it('should POST reqObj to syncDataDownloadUrl', () => { + const reqObj = { groupId: 1 }; + service.syncDownloadData(reqObj).subscribe(); + const req = httpMock.expectOne(environment.syncDataDownloadUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(reqObj); + req.flush({ statusCode: 200 }); + }); + }); + + describe('syncDownloadDataProgress', () => { + it('should make GET request to syncDownloadProgressUrl', () => { + service.syncDownloadDataProgress().subscribe(); + const req = httpMock.expectOne(environment.syncDownloadProgressUrl); + expect(req.request.method).toBe('GET'); + req.flush({ progress: 50 }); + }); + }); + + describe('getVanDetailsForMasterDownload', () => { + it('should make GET request to getVanDetailsForMasterDownloadUrl', () => { + service.getVanDetailsForMasterDownload().subscribe(); + const req = httpMock.expectOne( + environment.getVanDetailsForMasterDownloadUrl + ); + expect(req.request.method).toBe('GET'); + req.flush({ vanID: 1 }); + }); + }); + + describe('checkBenIDAvailability', () => { + it('should make GET request to getBenIDs', () => { + service.checkBenIDAvailability().subscribe(); + const req = httpMock.expectOne(environment.getBenIDs); + expect(req.request.method).toBe('GET'); + req.flush({ available: true }); + }); + }); + + describe('generateBenIDs', () => { + it('should POST reqObj to generateBenID', () => { + const reqObj = { count: 100, vanID: 5 }; + service.generateBenIDs(reqObj).subscribe(); + const req = httpMock.expectOne(environment.generateBenID); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(reqObj); + req.flush({ statusCode: 200 }); + }); + }); + + describe('inventorySyncDownloadData', () => { + it('should POST vanID to getInventorySyncData', () => { + service.inventorySyncDownloadData(10).subscribe(); + const req = httpMock.expectOne(environment.getInventorySyncData); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toBe(10); + req.flush({ statusCode: 200 }); + }); + }); }); diff --git a/src/app/app-modules/data-sync/shared/utility/data-sync-utility.spec.ts b/src/app/app-modules/data-sync/shared/utility/data-sync-utility.spec.ts new file mode 100644 index 00000000..79c988e8 --- /dev/null +++ b/src/app/app-modules/data-sync/shared/utility/data-sync-utility.spec.ts @@ -0,0 +1,48 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DataSyncUtils } from './data-sync-utility'; + +describe('DataSyncUtils', () => { + let utils: DataSyncUtils; + + beforeEach(() => { + utils = new DataSyncUtils(new FormBuilder()); + }); + + it('createBenIDForm builds a group with a null benID_Range control', () => { + const form = utils.createBenIDForm(); + expect(form instanceof FormGroup).toBeTrue(); + expect(Object.keys(form.controls)).toEqual(['benID_Range']); + expect(form.get('benID_Range')?.value).toBeNull(); + }); + + it('GenerateBenIDsForm nests the ben ID form under generateBenIDForm', () => { + const form: FormGroup = utils.GenerateBenIDsForm(); + const inner = form.get('generateBenIDForm') as FormGroup; + expect(inner instanceof FormGroup).toBeTrue(); + expect(inner.get('benID_Range')).toBeTruthy(); + form.patchValue({ generateBenIDForm: { benID_Range: 25 } }); + expect(form.value).toEqual({ generateBenIDForm: { benID_Range: 25 } }); + }); +}); diff --git a/src/app/app-modules/data-sync/workarea/workarea.component.spec.ts b/src/app/app-modules/data-sync/workarea/workarea.component.spec.ts index ae533521..a5985c7f 100644 --- a/src/app/app-modules/data-sync/workarea/workarea.component.spec.ts +++ b/src/app/app-modules/data-sync/workarea/workarea.component.spec.ts @@ -20,68 +20,364 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; import { FormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; -import { BehaviorSubject, of, throwError } from 'rxjs'; -import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; -import { ConfirmationService } from '../../core/services/confirmation.service'; -import { HttpServiceService } from '../../core/services/http-service.service'; -import { DataSyncService } from './../shared/service/data-sync.service'; +import { MatSelectModule } from '@angular/material/select'; +import { of } from 'rxjs'; import { WorkareaComponent } from './workarea.component'; +import { DataSyncService } from '../shared/service/data-sync.service'; +import { DataSyncUtils } from '../shared/utility/data-sync-utility'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; -describe('WorkareaComponent', () => { - let component: WorkareaComponent; +describe('DataSync WorkareaComponent', () => { let fixture: ComponentFixture; - let dataSyncService: jasmine.SpyObj; - let confirmationService: jasmine.SpyObj; - - const syncResponse = (data: any) => ({ - data, - statusCode: 200, - errorMessage: 'Success', - status: 'Success', - }); + let component: WorkareaComponent; + let dataSync: any; + let confirmation: any; + let session: any; + let router: Router; - beforeEach(async () => { - dataSyncService = jasmine.createSpyObj('DataSyncService', [ - 'syncDiagnosticDocuments', - 'getDataSYNCGroup', - ]); - dataSyncService.getDataSYNCGroup.and.returnValue(of({ data: [] }) as any); - confirmationService = jasmine.createSpyObj('ConfirmationService', [ - 'alert', - ]); + const groups = () => [ + { syncTableGroupID: 1, syncTableGroupName: 'A' }, + { syncTableGroupID: 2, syncTableGroupName: 'B' }, + ]; + async function create(sessionSeed: Record) { + dataSync = autoSpy(DataSyncService); + dataSync.getDataSYNCGroup.and.returnValue( + of({ statusCode: 200, data: groups() }) + ); await TestBed.configureTestingModule({ + // MatSelectModule supplies the value accessor for formControlName="benID_Range" + imports: [...COMMON_TEST_IMPORTS, MatSelectModule], declarations: [WorkareaComponent], providers: [ - FormBuilder, - { - provide: Router, - useValue: jasmine.createSpyObj('Router', ['navigate']), - }, - { provide: DataSyncService, useValue: dataSyncService }, - { provide: ConfirmationService, useValue: confirmationService }, - { - provide: HttpServiceService, - useValue: { currentLangugae$: new BehaviorSubject({}) }, - }, - { - provide: SessionStorageService, - useValue: { getItem: () => null, setItem: () => undefined }, - }, + ...commonTestProviders({ session: sessionSeed }), + { provide: DataSyncService, useValue: dataSync }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(WorkareaComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + fixture.detectChanges(); + } + + describe('without a server key', () => { + beforeEach(async () => create({})); + + it('redirects to the sync login', () => { + expect(router.navigate).toHaveBeenCalledWith(['datasync/sync-login']); + expect(dataSync.getDataSYNCGroup).not.toHaveBeenCalled(); + expect(component.generateBenIDForm.contains('benID_Range')).toBeTrue(); + }); + }); + + describe('with a server key', () => { + beforeEach(async () => + create({ + serverKey: 'k', + serviceLineDetails: JSON.stringify({ vanID: 7 }), + dataSyncProviderServiceMapID: 13, + }) + ); + + it('loads sync groups with sync flags reset', () => { + expect(router.navigate).not.toHaveBeenCalled(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.syncTableGroupList.length).toBe(2); + expect(component.syncTableGroupList[0]).toEqual( + jasmine.objectContaining({ benDetailSynced: false, visitSynced: false }) + ); + }); + + it('ignores a non-200 group response', () => { + component.syncTableGroupList = []; + dataSync.getDataSYNCGroup.and.returnValue(of({ statusCode: 500 })); + component.getDataSYNCGroup(); + expect(component.syncTableGroupList).toEqual([]); + }); + + it('ngOnDestroy removes the server key', () => { + spyOn(Storage.prototype, 'removeItem'); + component.ngOnDestroy(); + expect(Storage.prototype.removeItem).toHaveBeenCalledWith('serverKey'); + }); + + describe('syncDownloadData', () => { + it('does nothing when not confirmed', () => { + confirmation.confirm.and.returnValue(of(false)); + component.syncDownloadData(); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + 'Confirm to download data' + ); + expect(dataSync.syncDownloadData).not.toHaveBeenCalled(); + }); + + it('alerts the error on a non-200 response', () => { + dataSync.syncDownloadData.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.syncDownloadData(); + expect(dataSync.syncDownloadData).toHaveBeenCalledWith({ + vanID: 7, + providerServiceMapID: 13, + }); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(component.showProgressBar).toBeFalse(); + }); + + it('polls progress until 100% and lists failed masters', fakeAsync(() => { + dataSync.syncDownloadData.and.returnValue(of({ statusCode: 200 })); + dataSync.syncDownloadDataProgress.and.returnValues( + of({ statusCode: 200, data: { percentage: 40 } }), + of({ + statusCode: 200, + data: { percentage: 100, failedMasters: 'm1|m2| ' }, + }) + ); + component.syncDownloadData(); + expect(component.showProgressBar).toBeTrue(); + expect(component.canDeactivate()).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('Download in progress'); + + tick(2000); + expect(component.progressValue).toBe(40); + expect(component.showProgressBar).toBeTrue(); + + tick(2000); + expect(component.progressValue).toBe(100); + expect(component.failedMasterList).toEqual(['m1', 'm2']); + expect(component.showProgressBar).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Master download finished' + ); + expect(component.canDeactivate()).toBeTrue(); + + tick(4000); + expect(dataSync.syncDownloadDataProgress).toHaveBeenCalledTimes(2); + discardPeriodicTasks(); + })); + }); + + describe('syncDownloadProgressStatus', () => { + it('keeps the last failed master when it is not blank', () => { + dataSync.syncDownloadDataProgress.and.returnValue( + of({ + statusCode: 200, + data: { percentage: 100, failedMasters: 'm1|m2' }, + }) + ); + component.syncDownloadProgressStatus(); + expect(component.failedMasterList).toEqual(['m1', 'm2']); + }); + + it('ignores responses without data', () => { + dataSync.syncDownloadDataProgress.and.returnValue( + of({ statusCode: 200 }) + ); + component.progressValue = 5; + component.syncDownloadProgressStatus(); + expect(component.progressValue).toBe(5); + }); + }); + + describe('syncGroups', () => { + it('updates statuses and alerts success', () => { + dataSync.syncAllGroups.and.returnValue( + of({ + statusCode: 200, + data: { + response: 'done', + groupsProgress: [{ syncTableGroupID: 1, status: 'completed' }], + }, + }) + ); + component.syncGroups(); + expect(component.syncTableGroupList[0].status).toBe('success'); + expect(component.syncTableGroupList[1].status).toBe('pending'); + expect(confirmation.alert).toHaveBeenCalledWith('done', 'success'); + expect(component.showTable).toBeTrue(); + expect(component.displaySyncBool).toBeFalse(); + }); + + it('alerts success without progress info', () => { + dataSync.syncAllGroups.and.returnValue( + of({ statusCode: 200, data: { response: 'ok' } }) + ); + component.syncGroups(); + expect(component.syncTableGroupList[0].status).toBeUndefined(); + expect(confirmation.alert).toHaveBeenCalledWith('ok', 'success'); + }); + + it('alerts an error and still updates statuses on failure', () => { + dataSync.syncAllGroups.and.returnValue( + of({ + statusCode: 500, + data: { + response: 'partial fail', + groupsProgress: [ + { syncTableGroupID: 1, status: 'failed' }, + { syncTableGroupID: 2, status: 'partial' }, + ], + }, + }) + ); + component.syncGroups(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'partial fail', + 'error' + ); + expect(component.syncTableGroupList.map((g: any) => g.status)).toEqual([ + 'failed', + 'partial', + ]); + }); + + it('alerts an error without progress info', () => { + dataSync.syncAllGroups.and.returnValue( + of({ statusCode: 500, data: { response: 'nope' } }) + ); + component.syncGroups(); + expect(confirmation.alert).toHaveBeenCalledWith('nope', 'error'); + expect(component.showTable).toBeTrue(); + }); + + it('alerts the error message on HTTP failure', () => { + dataSync.syncAllGroups.and.returnValue( + throwingObs({ message: 'down' }) + ); + component.syncGroups(); + expect(confirmation.alert).toHaveBeenCalledWith('down', 'error'); + expect(component.showTable).toBeFalse(); + }); + + it('falls back to a generic error message', () => { + dataSync.syncAllGroups.and.returnValue(throwingObs({})); + component.syncGroups(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'An error occurred', + 'error' + ); + }); + }); + + it('updateGroupStatus maps unknown statuses to pending', () => { + component.updateGroupStatus([{ syncTableGroupID: 1, status: 'weird' }]); + expect(component.syncTableGroupList[0].status).toBe('pending'); + }); + + describe('checkBenIDAvailability', () => { + it('stores the available count', () => { + dataSync.checkBenIDAvailability.and.returnValue( + of({ data: { response: 42 } }) + ); + component.checkBenIDAvailability(); + expect(component.benID_Count).toBe(42); + }); + + it('alerts when nothing is returned', () => { + dataSync.checkBenIDAvailability.and.returnValue(of(null)); + component.checkBenIDAvailability(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'No benID available. Generate benIDs' + ); + }); + }); + + describe('generateBenID', () => { + it('refuses when more than 5000 benIDs are available', () => { + component.benID_Count = 6000; + component.generateBenID('10'); + expect(dataSync.generateBenIDs).not.toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + "Couldn't able to generate benIDs, count should be less than 5000" + ); + }); + + it('generates benIDs, refreshes the count and resets the range', () => { + component.benID_Count = 10; + component.generateBenIDForm.controls['benID_Range'].setValue('25'); + expect(component.benIDsRange).toBe('25'); + dataSync.checkBenIDAvailability.and.returnValue( + of({ data: { response: 35 } }) + ); + component.generateBenID('25'); + expect(dataSync.generateBenIDs).toHaveBeenCalledWith({ + vanID: 7, + benIDRequired: 25, + }); + expect(component.benID_Count).toBe(35); + expect(component.benIDsRange).toBeNull(); + }); + + it('does nothing further on an empty response', () => { + component.benID_Count = 10; + dataSync.generateBenIDs.and.returnValue(of(null)); + component.generateBenID('5'); + expect(dataSync.checkBenIDAvailability).not.toHaveBeenCalled(); + }); + }); + + describe('inventorySyncDataDownload', () => { + it('requests the download for the van and alerts on failure', () => { + dataSync.inventorySyncDownloadData.and.returnValue( + of({ statusCode: 500, errorMessage: 'inv fail' }) + ); + component.inventorySyncDataDownload(); + expect(dataSync.inventorySyncDownloadData).toHaveBeenCalledWith({ + vanID: 7, + }); + expect(confirmation.alert).toHaveBeenCalledWith('inv fail', 'error'); + }); + + it('does not alert on success', () => { + dataSync.inventorySyncDownloadData.and.returnValue( + of({ statusCode: 200 }) + ); + component.inventorySyncDataDownload(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('does nothing when not confirmed', () => { + confirmation.confirm.and.returnValue(of(false)); + component.inventorySyncDataDownload(); + expect(dataSync.inventorySyncDownloadData).not.toHaveBeenCalled(); + }); + + it('sends an undefined vanID when no service line is stored', () => { + session.store.delete('serviceLineDetails'); + component.inventorySyncDataDownload(); + expect(dataSync.inventorySyncDownloadData).toHaveBeenCalledWith({ + vanID: undefined, + }); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('DataSyncUtils builds the wrapper form', () => { + const form = new DataSyncUtils(new FormBuilder()).GenerateBenIDsForm(); + expect(form.get('generateBenIDForm.benID_Range')).toBeTruthy(); }); describe('startDocumentSync', () => { diff --git a/src/app/app-modules/lab/dashboard/dashboard.component.spec.ts b/src/app/app-modules/lab/dashboard/dashboard.component.spec.ts index bd7f10ea..4a9a8541 100644 --- a/src/app/app-modules/lab/dashboard/dashboard.component.spec.ts +++ b/src/app/app-modules/lab/dashboard/dashboard.component.spec.ts @@ -20,27 +20,31 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DashboardComponent } from './dashboard.component'; -describe('DashboardComponent', () => { +describe('lab DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [DashboardComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('renders the shell: header with roles, router outlet and footer', () => { expect(component).toBeTruthy(); + const el: HTMLElement = fixture.nativeElement; + const header = el.querySelector('app-header'); + expect(header).not.toBeNull(); + expect(el.querySelector('router-outlet')).not.toBeNull(); + expect(el.querySelector('app-footer')).not.toBeNull(); }); }); diff --git a/src/app/app-modules/lab/shared/services/lab.service.spec.ts b/src/app/app-modules/lab/shared/services/lab.service.spec.ts index 37601600..a04ed37e 100644 --- a/src/app/app-modules/lab/shared/services/lab.service.spec.ts +++ b/src/app/app-modules/lab/shared/services/lab.service.spec.ts @@ -20,18 +20,105 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { LabService } from './lab.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; describe('LabService', () => { + let service: LabService; + let httpMock: HttpTestingController; + let mockSessionStorage: jasmine.SpyObj; + beforeEach(() => { + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + TestBed.configureTestingModule({ - providers: [LabService], + imports: [HttpClientTestingModule], + providers: [ + LabService, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], }); + + service = TestBed.inject(LabService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); }); - it('should be created', inject([LabService], (service: LabService) => { + it('should be created', () => { expect(service).toBeTruthy(); - })); + }); + + describe('getLabWorklist', () => { + it('should make GET request with correct URL params', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return JSON.stringify({ vanID: 10 }); + if (key === 'providerServiceID') return '100'; + if (key === 'serviceID') return '2'; + return null; + }); + + service.getLabWorklist().subscribe(); + const expectedUrl = environment.labWorklist + '100/2/10'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ statusCode: 200, data: [] }); + }); + }); + + describe('getEcgAbnormalities', () => { + it('should make GET request to getEcgAbnormalitiesMasterUrl', () => { + service.getEcgAbnormalities().subscribe(); + const req = httpMock.expectOne(environment.getEcgAbnormalitiesMasterUrl); + expect(req.request.method).toBe('GET'); + req.flush({ statusCode: 200, data: [] }); + }); + }); + + describe('saveLabWork', () => { + it('should POST tech form data to labSaveWork URL', () => { + const techForm = { labTestID: 1, result: 'Positive' }; + service.saveLabWork(techForm).subscribe(); + const req = httpMock.expectOne(environment.labSaveWork); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(techForm); + req.flush({ statusCode: 200 }); + }); + }); + + describe('saveFile', () => { + it('should POST file data to saveFile URL', () => { + const file = { fileName: 'test.pdf', fileContent: 'base64data' }; + service.saveFile(file).subscribe(); + const req = httpMock.expectOne(environment.saveFile); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(file); + req.flush({ statusCode: 200 }); + }); + }); + + describe('viewFileContent', () => { + it('should POST viewFileIndex and return blob', () => { + const viewFileIndex = { fileID: 123 }; + service.viewFileContent(viewFileIndex).subscribe(res => { + expect(res).toBeTruthy(); + }); + const req = httpMock.expectOne(environment.viewFileData); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(viewFileIndex); + expect(req.request.responseType).toBe('blob'); + req.flush(new Blob(['test'])); + }); + }); }); diff --git a/src/app/app-modules/lab/shared/services/master-data.service.spec.ts b/src/app/app-modules/lab/shared/services/master-data.service.spec.ts index 1c013dda..6560592c 100644 --- a/src/app/app-modules/lab/shared/services/master-data.service.spec.ts +++ b/src/app/app-modules/lab/shared/services/master-data.service.spec.ts @@ -20,21 +20,57 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { MasterDataService } from './master-data.service'; +import { environment } from 'src/environments/environment'; describe('MasterDataService', () => { + let service: MasterDataService; + let httpMock: HttpTestingController; + beforeEach(() => { TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], providers: [MasterDataService], }); + + service = TestBed.inject(MasterDataService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); }); - it('should be created', inject( - [MasterDataService], - (service: MasterDataService) => { - expect(service).toBeTruthy(); - } - )); + describe('getLabRequirements', () => { + it('should POST beneficiary details to get prescribed test data', () => { + service.getLabRequirements('12345', '100', 'VC001').subscribe(res => { + expect(res).toBeTruthy(); + }); + + const req = httpMock.expectOne(environment.getprescribedTestDataUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + beneficiaryRegID: '12345', + benVisitID: '100', + visitCode: 'VC001', + }); + req.flush({ statusCode: 200, data: [] }); + }); + + it('should handle null parameters', () => { + service.getLabRequirements(null, null, null).subscribe(); + const req = httpMock.expectOne(environment.getprescribedTestDataUrl); + expect(req.request.body.beneficiaryRegID).toBeNull(); + req.flush({ statusCode: 200 }); + }); + }); }); diff --git a/src/app/app-modules/lab/shared/utility/lab-utility.spec.ts b/src/app/app-modules/lab/shared/utility/lab-utility.spec.ts new file mode 100644 index 00000000..06efcaf9 --- /dev/null +++ b/src/app/app-modules/lab/shared/utility/lab-utility.spec.ts @@ -0,0 +1,77 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { LabUtils } from './lab-utility'; + +describe('LabUtils', () => { + let utils: LabUtils; + beforeEach(() => (utils = new LabUtils(new FormBuilder()))); + + it('createLabMasterForm has three empty arrays', () => { + const f = utils.createLabMasterForm(); + ['labForm', 'radiologyForm', 'externalForm'].forEach(k => { + expect(f.get(k) instanceof FormArray).toBeTrue(); + expect((f.get(k) as FormArray).length).toBe(0); + }); + }); + + it('createLabProcedureForm has procedure fields and compListDetails array', () => { + const f = utils.createLabProcedureForm(); + expect(f.get('procedureID')?.value).toBeNull(); + expect(f.get('calibrationEndAPI')).toBeTruthy(); + expect(f.get('compListDetails') instanceof FormArray).toBeTrue(); + }); + + it('createLabComponentOfFields has field controls', () => { + const f = utils.createLabComponentOfFields(); + expect(Object.keys(f.controls).length).toBe(16); + expect(f.get('stripsNotavailable')?.value).toBeNull(); + expect(f.get('range_normal_min')).toBeTruthy(); + }); + + it('createLabComponentOfRadioDropDowns has compOpt array', () => { + const f = utils.createLabComponentOfRadioDropDowns(); + expect(f.get('compOpt') instanceof FormArray).toBeTrue(); + expect(f.get('compOptSelected')?.value).toBeNull(); + expect(f.get('ecgAbnormalities')?.value).toBeNull(); + }); + + it('createComponentRadioDropDownList has name', () => { + expect(utils.createComponentRadioDropDownList().value).toEqual({ + name: null, + }); + }); + + it('createRadiologyProcedureForm nests radiology component', () => { + const f = utils.createRadiologyProcedureForm(); + expect(f.get('gender')).toBeTruthy(); + const comp = f.get('compDetails') as FormGroup; + expect(comp instanceof FormGroup).toBeTrue(); + expect(comp.value).toEqual(utils.createRadiologyComponent().value); + expect(Object.keys(comp.controls)).toContain('testComponentName'); + }); + + it('createExternalTestForm has tests control', () => { + expect(utils.createExternalTestForm().value).toEqual({ tests: null }); + }); +}); diff --git a/src/app/app-modules/lab/view-file/view-file.component.spec.ts b/src/app/app-modules/lab/view-file/view-file.component.spec.ts index b32c8235..6d483480 100644 --- a/src/app/app-modules/lab/view-file/view-file.component.spec.ts +++ b/src/app/app-modules/lab/view-file/view-file.component.spec.ts @@ -20,17 +20,54 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; import { ViewFileComponent } from './view-file.component'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; describe('ViewFileComponent', () => { let component: ViewFileComponent; let fixture: ComponentFixture; + let mockDialogRef: jasmine.SpyObj>; + let mockConfirmationService: jasmine.SpyObj; + let mockHttpServiceService: jasmine.SpyObj; + + const mockDialogData = { + viewFileObj: { + '123': [ + { fileName: 'file1.pdf', filePath: '/path/to/file1.pdf' }, + { fileName: 'file2.pdf', filePath: '/path/to/file2.pdf' }, + ], + }, + procedureID: '123', + }; + + beforeEach(waitForAsync(() => { + mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockHttpServiceService = jasmine.createSpyObj('HttpServiceService', [ + 'setLanguage', + 'currentLangugae$', + ]); + + mockHttpServiceService.currentLangugae$ = of({ languageID: 1 }); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ViewFileComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: mockDialogData }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -43,4 +80,111 @@ describe('ViewFileComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('constructor', () => { + it('should set dialogRef.disableClose to true', () => { + expect(mockDialogRef.disableClose).toBe(true); + }); + }); + + describe('ngOnInit', () => { + it('should set fileObj from input viewFileObj using procedureID', () => { + expect(component.fileObj).toBeDefined(); + expect(component.fileObj.length).toBe(2); + }); + + it('should call assignObject with viewFileObj and procedureID', () => { + spyOn(component, 'assignObject'); + component.ngOnInit(); + expect(component.assignObject).toHaveBeenCalledWith( + mockDialogData.viewFileObj, + mockDialogData.procedureID + ); + }); + }); + + describe('assignObject', () => { + it('should set fileObj to inputFileObj[procedureID]', () => { + const inputFileObj = { + '456': [{ fileName: 'test.pdf' }], + }; + component.assignObject(inputFileObj, '456'); + expect(component.fileObj).toEqual([{ fileName: 'test.pdf' }]); + }); + + it('should set fileObj to undefined for non-existent procedureID', () => { + const inputFileObj = { + '456': [{ fileName: 'test.pdf' }], + }; + component.assignObject(inputFileObj, '999'); + expect(component.fileObj).toBeUndefined(); + }); + }); + + describe('remove', () => { + beforeEach(() => { + component.current_language_set = { + alerts: { info: { wantToRemoveFile: 'Do you want to remove?' } }, + }; + // Reset the viewFileObj array for each test + component.input.viewFileObj = { + '123': [ + { fileName: 'file1.pdf', filePath: '/path/to/file1.pdf' }, + { fileName: 'file2.pdf', filePath: '/path/to/file2.pdf' }, + ], + }; + component.input.procedureID = '123'; + }); + + it('should call confirmationService.confirm with correct message', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const file = component.input.viewFileObj['123'][0]; + component.remove(file); + expect(mockConfirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Do you want to remove?' + ); + }); + + it('should remove file from array on confirmation', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const file = component.input.viewFileObj['123'][0]; + component.remove(file); + expect(component.input.viewFileObj['123'].length).toBe(1); + expect(component.input.viewFileObj['123'][0].fileName).toBe('file2.pdf'); + }); + + it('should not remove file when user declines', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + const file = component.input.viewFileObj['123'][0]; + component.remove(file); + expect(component.input.viewFileObj['123'].length).toBe(2); + }); + + it('should close dialog when array becomes empty after removal', () => { + component.input.viewFileObj = { + '123': [{ fileName: 'only-file.pdf', filePath: '/path/only.pdf' }], + }; + mockConfirmationService.confirm.and.returnValue(of(true)); + const file = component.input.viewFileObj['123'][0]; + component.remove(file); + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + + it('should not close dialog when array still has items after removal', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const file = component.input.viewFileObj['123'][0]; + component.remove(file); + expect(mockDialogRef.close).not.toHaveBeenCalled(); + }); + }); + + describe('closeDialog', () => { + it('should close dialog with viewFileObj as returnObj', () => { + component.closeDialog(); + expect(mockDialogRef.close).toHaveBeenCalledWith( + component.input.viewFileObj + ); + }); + }); }); diff --git a/src/app/app-modules/lab/workarea/LabSubmissionDataManipulation.spec.ts b/src/app/app-modules/lab/workarea/LabSubmissionDataManipulation.spec.ts new file mode 100644 index 00000000..4f649eba --- /dev/null +++ b/src/app/app-modules/lab/workarea/LabSubmissionDataManipulation.spec.ts @@ -0,0 +1,197 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { DataManipulation } from './LabSubmissionDataManipulation'; + +describe('DataManipulation (lab submission)', () => { + let dm: DataManipulation; + beforeEach(() => (dm = new DataManipulation())); + + describe('technicalDataRestruct', () => { + it('restructures lab results and leaves radiology undefined', () => { + const out = dm.technicalDataRestruct({ + labForm: [ + { + prescriptionID: 1, + procedureID: 2, + compListDetails: [ + { testComponentName: 'Hb', testComponentID: 3, inputValue: '12' }, + ], + }, + ], + }); + expect(out).toEqual({ + labTestResults: [ + { + prescriptionID: 1, + procedureID: 2, + compList: [ + { + testComponentID: 3, + testResultValue: '12', + testResultUnit: undefined, + remarks: undefined, + }, + ], + }, + ], + radiologyTestResults: undefined, + }); + }); + + it('returns undefined lab results when no lab form is present', () => { + expect(dm.technicalDataRestruct({})).toEqual({ + labTestResults: undefined, + radiologyTestResults: undefined, + }); + }); + }); + + describe('laboratoryDataRestruct', () => { + it('skips procedures without ids and procedures with no filled components', () => { + const out = dm.laboratoryDataRestruct([ + { prescriptionID: null, procedureID: 1, compListDetails: [] }, + { + prescriptionID: 1, + procedureID: 2, + compListDetails: [{ testComponentName: 'Hb', testComponentID: 3 }], + }, + { + prescriptionID: 4, + procedureID: 5, + compListDetails: [ + { + testComponentName: 'Sugar', + testComponentID: 6, + compOptSelected: 'High', + }, + ], + }, + ]); + expect(out.length).toBe(1); + expect(out[0].procedureID).toBe(5); + expect(out[0].compList[0].testResultValue).toBe('High'); + }); + }); + + describe('labComponentRestruct', () => { + it('strips unavailable + abnormal ECG keeps abnormalities and flag', () => { + const [c] = dm.labComponentRestruct([ + { + stripsNotavailable: true, + testComponentName: 'ECG Report', + testComponentID: 1, + compOptSelected: 'Abnormal', + ecgAbnormalities: ['ST elevation'], + measurementUnit: 'mm', + remarks: 'r', + }, + ]); + expect(c).toEqual({ + testComponentID: 1, + testResultValue: 'Abnormal', + testResultUnit: 'mm', + ecgAbnormalities: ['ST elevation'], + remarks: 'r', + stripsNotAvailable: true, + }); + }); + + it('strips unavailable for a regular component keeps the flag', () => { + const [c] = dm.labComponentRestruct([ + { + stripsNotavailable: true, + testComponentName: 'Hb', + testComponentID: 2, + }, + ]); + expect(c).toEqual({ + testComponentID: 2, + testResultValue: undefined, + testResultUnit: undefined, + remarks: undefined, + stripsNotAvailable: true, + }); + }); + + it('strips unavailable without a component id is dropped', () => { + expect( + dm.labComponentRestruct([ + { stripsNotavailable: true, testComponentName: 'Hb' }, + ]) + ).toEqual([]); + }); + + it('abnormal ECG (strips available) includes abnormalities', () => { + const [c] = dm.labComponentRestruct([ + { + testComponentName: 'ECG', + testComponentID: 3, + compOptSelected: 'Abnormal', + ecgAbnormalities: ['AF'], + }, + ]); + expect(c.ecgAbnormalities).toEqual(['AF']); + expect(c.testResultValue).toBe('Abnormal'); + expect(c.stripsNotAvailable).toBeUndefined(); + }); + + it('normal ECG is treated like a regular option component', () => { + const [c] = dm.labComponentRestruct([ + { + testComponentName: 'ECG', + testComponentID: 4, + compOptSelected: 'Normal', + ecgAbnormalities: ['ignored'], + }, + ]); + expect(c).toEqual({ + testComponentID: 4, + testResultValue: 'Normal', + testResultUnit: undefined, + remarks: undefined, + }); + }); + + it('prefers inputValue over the selected option and drops empty components', () => { + const comps = dm.labComponentRestruct([ + { + testComponentName: 'Hb', + testComponentID: 5, + inputValue: '13', + compOptSelected: 'x', + measurementUnit: 'g/dL', + }, + { testComponentName: 'Empty', testComponentID: 6 }, + { testComponentName: 'NoId', inputValue: '1' }, + ]); + expect(comps).toEqual([ + { + testComponentID: 5, + testResultValue: '13', + testResultUnit: 'g/dL', + remarks: undefined, + }, + ]); + }); + }); +}); diff --git a/src/app/app-modules/lab/workarea/workarea.canactivate.service.spec.ts b/src/app/app-modules/lab/workarea/workarea.canactivate.service.spec.ts new file mode 100644 index 00000000..a78c6dbf --- /dev/null +++ b/src/app/app-modules/lab/workarea/workarea.canactivate.service.spec.ts @@ -0,0 +1,109 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { WorkareaCanActivate } from './workarea.canactivate.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +describe('WorkareaCanActivate (lab)', () => { + let guard: WorkareaCanActivate; + let sessionStorageSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + const requiredKeys = [ + 'visitCode', + 'benFlowID', + 'visitCategory', + 'beneficiaryRegID', + 'visitID', + 'beneficiaryID', + 'doctorFlag', + 'nurseFlag', + ]; + + beforeEach(() => { + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + providers: [ + WorkareaCanActivate, + { provide: Router, useValue: routerSpy }, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + ], + }); + guard = TestBed.inject(WorkareaCanActivate); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + it('should return true when all required session keys are present', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + const store: Record = { + visitCode: 'VC001', + benFlowID: '222', + visitCategory: 'General OPD', + beneficiaryRegID: '9876', + visitID: '111', + beneficiaryID: '333', + doctorFlag: '1', + nurseFlag: '2', + }; + return store[key] || null; + }); + + const result = guard.canActivate(null as any, null as any); + expect(result).toBeTrue(); + }); + + requiredKeys.forEach(missingKey => { + it(`should return false when '${missingKey}' is missing`, () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === missingKey) return null; + const store: Record = { + visitCode: 'VC001', + benFlowID: '222', + visitCategory: 'General OPD', + beneficiaryRegID: '9876', + visitID: '111', + beneficiaryID: '333', + doctorFlag: '1', + nurseFlag: '2', + }; + return store[key] || null; + }); + + const result = guard.canActivate(null as any, null as any); + expect(result).toBeFalse(); + }); + }); + + it('should return false when all session keys are missing', () => { + sessionStorageSpy.getItem.and.returnValue(null); + const result = guard.canActivate(null as any, null as any); + expect(result).toBeFalse(); + }); +}); diff --git a/src/app/app-modules/lab/workarea/workarea.component.spec.ts b/src/app/app-modules/lab/workarea/workarea.component.spec.ts index b8f77010..cfae8340 100644 --- a/src/app/app-modules/lab/workarea/workarea.component.spec.ts +++ b/src/app/app-modules/lab/workarea/workarea.component.spec.ts @@ -20,27 +20,1333 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + FormBuilder, + FormGroup, + FormArray, + ReactiveFormsModule, +} from '@angular/forms'; +import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { of, throwError } from 'rxjs'; import { WorkareaComponent } from './workarea.component'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { MasterDataService, LabService } from '../shared/services'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; describe('WorkareaComponent', () => { let component: WorkareaComponent; let fixture: ComponentFixture; + let confirmationService: jasmine.SpyObj; + let mockDialog: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockMasterDataService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockLabService: jasmine.SpyObj; + let mockHttpServiceService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + + const sessionStorageData: Record = { + visitID: '100', + visitCode: '200', + beneficiaryRegID: '300', + }; + + beforeEach(waitForAsync(() => { + confirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockMasterDataService = jasmine.createSpyObj('MasterDataService', [ + 'getLabRequirements', + ]); + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['getBeneficiaryDetails'] + ); + mockLabService = jasmine.createSpyObj('LabService', [ + 'saveFile', + 'saveLabWork', + 'viewFileContent', + 'getEcgAbnormalities', + ]); + mockHttpServiceService = jasmine.createSpyObj( + 'HttpServiceService', + ['getCurrentLanguage'], + { currentLangugae$: of({}) } + ); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + + mockSessionStorageService.getItem.and.callFake((key: string) => { + return (sessionStorageData as any)[key] || null; + }); + + mockMasterDataService.getLabRequirements.and.returnValue(of({})); + mockLabService.getEcgAbnormalities.and.returnValue(of({})); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [WorkareaComponent], + imports: [ReactiveFormsModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + FormBuilder, + { provide: ConfirmationService, useValue: confirmationService }, + { provide: MatDialog, useValue: mockDialog }, + { provide: Router, useValue: mockRouter }, + { provide: MasterDataService, useValue: mockMasterDataService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { provide: LabService, useValue: mockLabService }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + { + provide: SessionStorageService, + useValue: mockSessionStorageService, + }, + ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(WorkareaComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should read visitID, visitCode, and beneficiaryRegID from sessionStorage', () => { + fixture.detectChanges(); + + expect(mockSessionStorageService.getItem).toHaveBeenCalledWith('visitID'); + expect(mockSessionStorageService.getItem).toHaveBeenCalledWith( + 'visitCode' + ); + expect(mockSessionStorageService.getItem).toHaveBeenCalledWith( + 'beneficiaryRegID' + ); + expect(component.visitID).toBe('100'); + expect(component.visitCode).toBe('200'); + expect(component.beneficiaryRegID).toBe('300'); + }); + + it('should set stepExpand to 0', () => { + fixture.detectChanges(); + expect(component.stepExpand).toBe(0); + }); + + it('should set testName from environment.RBSTest', () => { + fixture.detectChanges(); + expect(component.testName).toBe(environment.RBSTest); + }); + + it('should call getTestRequirements', () => { + spyOn(component, 'getTestRequirements'); + component.ngOnInit(); + expect(component.getTestRequirements).toHaveBeenCalled(); + }); + + it('should call getEcgAbnormalities', () => { + spyOn(component, 'getEcgAbnormalities'); + component.ngOnInit(); + expect(component.getEcgAbnormalities).toHaveBeenCalled(); + }); + }); + + describe('errorLoading', () => { + it('should alert with the provided error message', () => { + const errorMsg = 'Something went wrong'; + component.errorLoading(errorMsg); + expect(confirmationService.alert).toHaveBeenCalledWith(errorMsg); + }); + + it('should navigate to /lab/worklist', () => { + component.errorLoading('error'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/lab/worklist']); + }); + }); + + describe('checkExtension', () => { + it('should return true for a valid pdf file', () => { + const file = { name: 'report.pdf' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid png file', () => { + const file = { name: 'image.png' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid jpeg file', () => { + const file = { name: 'photo.jpeg' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid doc file', () => { + const file = { name: 'document.doc' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid xlsx file', () => { + const file = { name: 'spreadsheet.xlsx' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid csv file', () => { + const file = { name: 'data.csv' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return true for a valid txt file', () => { + const file = { name: 'notes.txt' }; + expect(component.checkExtension(file)).toBe(true); + }); + + it('should return false for an invalid extension like exe', () => { + const file = { name: 'virus.exe' }; + expect(component.checkExtension(file)).toBe(false); + }); + + it('should return false for an invalid extension like bat', () => { + const file = { name: 'script.bat' }; + expect(component.checkExtension(file)).toBe(false); + }); + + it('should return false for a file with multiple dots (more than two parts)', () => { + const file = { name: 'report.v2.pdf' }; + expect(component.checkExtension(file)).toBe(false); + }); + + it('should return true when file is null or undefined', () => { + expect(component.checkExtension(null)).toBe(true); + expect(component.checkExtension(undefined)).toBe(true); + }); + + it('should be case-insensitive for extensions', () => { + const file = { name: 'report.PDF' }; + expect(component.checkExtension(file)).toBe(true); + }); + }); + + describe('canDeactivate', () => { + beforeEach(() => { + const fb = TestBed.inject(FormBuilder); + component.technicianForm = fb.group({ + labForm: fb.array([]), + radiologyForm: fb.array([]), + externalForm: fb.array([]), + }); + component.current_language_set = { + alerts: { + info: { + navigateFurtherAlert: + 'You have unsaved data. Do you want to navigate?', + }, + }, + }; + }); + + it('should return of(true) when form is not dirty', done => { + component.canDeactivate().subscribe(result => { + expect(result).toBe(true); + done(); + }); + }); + + it('should call confirmationService.confirm when form is dirty', () => { + confirmationService.confirm.and.returnValue(of(true)); + component.technicianForm.markAsDirty(); + + component.canDeactivate().subscribe(); + + expect(confirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'You have unsaved data. Do you want to navigate?', + 'Yes', + 'No' + ); + }); + + it('should return the result from confirmationService.confirm when form is dirty', done => { + confirmationService.confirm.and.returnValue(of(false)); + component.technicianForm.markAsDirty(); + + component.canDeactivate().subscribe(result => { + expect(result).toBe(false); + done(); + }); + }); + }); + + describe('sideNavModeChange', () => { + it('should set sidenav mode to "over" when device width is less than 700', () => { + const sidenav = { mode: '', toggle: jasmine.createSpy('toggle') }; + spyOnProperty(window.screen, 'width', 'get').and.returnValue(500); + + component.sideNavModeChange(sidenav); + + expect(sidenav.mode).toBe('over'); + expect(sidenav.toggle).toHaveBeenCalled(); + }); + + it('should set sidenav mode to "side" when device width is 700 or more', () => { + const sidenav = { mode: '', toggle: jasmine.createSpy('toggle') }; + spyOnProperty(window.screen, 'width', 'get').and.returnValue(1024); + + component.sideNavModeChange(sidenav); + + expect(sidenav.mode).toBe('side'); + expect(sidenav.toggle).toHaveBeenCalled(); + }); + + it('should set sidenav mode to "side" when device width is exactly 700', () => { + const sidenav = { mode: '', toggle: jasmine.createSpy('toggle') }; + spyOnProperty(window.screen, 'width', 'get').and.returnValue(700); + + component.sideNavModeChange(sidenav); + + expect(sidenav.mode).toBe('side'); + expect(sidenav.toggle).toHaveBeenCalled(); + }); + }); + + describe('filterProceduresLab', () => { + beforeEach(() => { + component.filteredLaboratoryData = [ + { procedureName: 'Blood Glucose Test' }, + { procedureName: 'Hemoglobin Count' }, + { procedureName: 'Blood Pressure' }, + ]; + }); + + it('should reset laboratoryData to full list when searchTerm is empty', () => { + component.filterProceduresLab(''); + expect(component.laboratoryData.data).toEqual( + component.filteredLaboratoryData + ); + }); + + it('should reset laboratoryData to full list when searchTerm is undefined', () => { + component.filterProceduresLab(undefined); + expect(component.laboratoryData.data).toEqual( + component.filteredLaboratoryData + ); + }); + + it('should filter laboratory data by procedureName matching search term', () => { + component.filterProceduresLab('Blood'); + expect(component.laboratoryData.data.length).toBe(2); + expect(component.laboratoryData.data).toContain( + jasmine.objectContaining({ procedureName: 'Blood Glucose Test' }) + ); + expect(component.laboratoryData.data).toContain( + jasmine.objectContaining({ procedureName: 'Blood Pressure' }) + ); + }); + + it('should be case-insensitive when filtering', () => { + component.filterProceduresLab('hemoglobin'); + expect(component.laboratoryData.data.length).toBe(1); + expect(component.laboratoryData.data[0].procedureName).toBe( + 'Hemoglobin Count' + ); + }); + + it('should return empty data when no match found', () => { + component.filterProceduresLab('XYZ'); + expect(component.laboratoryData.data.length).toBe(0); + }); + }); + + describe('filterProceduresRadiology', () => { + beforeEach(() => { + component.filteredRadiologyData = [ + { procedureName: 'X-Ray Chest' }, + { procedureName: 'CT Scan' }, + { procedureName: 'X-Ray Spine' }, + ]; + }); + + it('should reset radiologyFile data to full list when searchTerm is empty', () => { + component.filterProceduresRadiology(''); + expect(component.radiologyFile.data).toEqual( + component.filteredRadiologyData + ); + }); + + it('should reset radiologyFile data to full list when searchTerm is undefined', () => { + component.filterProceduresRadiology(undefined); + expect(component.radiologyFile.data).toEqual( + component.filteredRadiologyData + ); + }); + + it('should filter radiology data by procedureName matching search term', () => { + component.filterProceduresRadiology('X-Ray'); + expect(component.radiologyFile.data.length).toBe(2); + expect(component.radiologyFile.data).toContain( + jasmine.objectContaining({ procedureName: 'X-Ray Chest' }) + ); + expect(component.radiologyFile.data).toContain( + jasmine.objectContaining({ procedureName: 'X-Ray Spine' }) + ); + }); + + it('should be case-insensitive when filtering', () => { + component.filterProceduresRadiology('ct scan'); + expect(component.radiologyFile.data.length).toBe(1); + expect(component.radiologyFile.data[0].procedureName).toBe('CT Scan'); + }); + + it('should return empty data when no match found', () => { + component.filterProceduresRadiology('MRI'); + expect(component.radiologyFile.data.length).toBe(0); + }); + }); + + describe('loadArchive', () => { + it('should not modify archiveList when archive is null', () => { + component.archiveList = []; + component.loadArchive(null); + expect(component.archiveList).toEqual([]); + }); + + it('should not modify archiveList when archive is undefined', () => { + component.archiveList = []; + component.loadArchive(undefined); + expect(component.archiveList).toEqual([]); + }); + + it('should not modify archiveList when archive is empty array', () => { + component.archiveList = []; + component.loadArchive([]); + expect(component.archiveList).toEqual([]); + }); + + it('should set archiveList and filteredArchiveList when archive has data', () => { + const archive = [ + { procedureType: 'Radiology', procedureName: 'X-Ray' }, + { procedureType: 'Laboratory', procedureName: 'Blood Test' }, + ]; + + component.loadArchive(archive); + + expect(component.archiveList).toBe(archive); + expect(component.filteredArchiveList).toBe(archive); + }); + + it('should split archive into radiology and laboratory data sources', () => { + const archive = [ + { procedureType: 'Radiology', procedureName: 'X-Ray' }, + { procedureType: 'Laboratory', procedureName: 'Blood Test' }, + { procedureType: 'Radiology', procedureName: 'CT Scan' }, + ]; + + component.loadArchive(archive); + + expect(component.radiologyFile.data.length).toBe(2); + expect(component.laboratoryData.data.length).toBe(1); + expect(component.filteredRadiologyData.length).toBe(2); + expect(component.filteredLaboratoryData.length).toBe(1); + }); + }); + + describe('checkForEcg', () => { + beforeEach(() => { + const fb = TestBed.inject(FormBuilder); + component.technicianForm = fb.group({ + labForm: fb.array([ + fb.group({ + procedureName: 'ECG Test', + compListDetails: fb.array([fb.group({ ecgAbnormalities: [] })]), + }), + ]), + radiologyForm: fb.array([]), + externalForm: fb.group({}), + }); + }); + + it('should set enableEcgAbnormal to true when value is "Abnormal" and procedure includes "ECG"', () => { + component.checkForEcg('Abnormal', 'ECG Test', 0); + expect(component.enableEcgAbnormal).toBe(true); + }); + + it('should set enableEcgAbnormal to false when value is not "Abnormal" and procedure includes "ECG"', () => { + component.enableEcgAbnormal = true; + component.checkForEcg('Normal', 'ECG Test', 0); + expect(component.enableEcgAbnormal).toBe(false); + }); + + it('should not change enableEcgAbnormal when procedure name does not include "ECG"', () => { + component.enableEcgAbnormal = false; + component.checkForEcg('Abnormal', 'Blood Pressure', 0); + expect(component.enableEcgAbnormal).toBe(false); + }); + + it('should clear ecgAbnormalities in compListDetails when value is not "Abnormal" for ECG procedure', () => { + const labFormValue = component.technicianForm.controls['labForm'].value; + component.checkForEcg('Normal', 'ECG Test', 0); + + expect(labFormValue[0].compListDetails[0].ecgAbnormalities).toEqual([]); + }); + }); + + describe('getEcgAbnormalities', () => { + it('should set ecgAbnormalities when service returns status 200 with data', () => { + const mockData = [{ id: 1, name: 'Sinus Tachycardia' }]; + mockLabService.getEcgAbnormalities.and.returnValue( + of({ statusCode: 200, data: mockData }) + ); + + component.getEcgAbnormalities(); + + expect(component.ecgAbnormalities).toEqual(mockData); + }); + + it('should not set ecgAbnormalities when service returns data as null', () => { + mockLabService.getEcgAbnormalities.and.returnValue( + of({ statusCode: 200, data: null }) + ); + + component.ecgAbnormalities = undefined; + component.getEcgAbnormalities(); + + expect(component.ecgAbnormalities).toBeUndefined(); + }); + + it('should not set ecgAbnormalities when statusCode is not 200', () => { + mockLabService.getEcgAbnormalities.and.returnValue( + of({ statusCode: 500, errorMessage: 'Server Error' }) + ); + + component.ecgAbnormalities = undefined; + component.getEcgAbnormalities(); + + expect(component.ecgAbnormalities).toBeUndefined(); + }); + }); + + describe('confirmFormReset', () => { + beforeEach(() => { + const fb = TestBed.inject(FormBuilder); + component.technicianForm = fb.group({ + labForm: fb.array([]), + radiologyForm: fb.array([]), + externalForm: fb.array([]), + }); + component.current_language_set = { + alerts: { + info: { + resetDetails: 'Are you sure you want to reset?', + }, + }, + }; + }); + + it('should not call confirmationService.confirm when form is not dirty', () => { + component.confirmFormReset(); + expect(confirmationService.confirm).not.toHaveBeenCalled(); + }); + + it('should call confirmationService.confirm when form is dirty', () => { + confirmationService.confirm.and.returnValue(of(false)); + component.technicianForm.markAsDirty(); + + component.confirmFormReset(); + + expect(confirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Are you sure you want to reset?' + ); + }); + + it('should call formReCall when user confirms reset', () => { + confirmationService.confirm.and.returnValue(of(true)); + component.technicianForm.markAsDirty(); + spyOn(component, 'formReCall'); + + component.confirmFormReset(); + + expect(component.formReCall).toHaveBeenCalled(); + }); + + it('should not call formReCall when user cancels reset', () => { + confirmationService.confirm.and.returnValue(of(false)); + component.technicianForm.markAsDirty(); + spyOn(component, 'formReCall'); + + component.confirmFormReset(); + + expect(component.formReCall).not.toHaveBeenCalled(); + }); + }); + + describe('getTestRequirements', () => { + it('should call masterdataService.getLabRequirements when visitID, beneficiaryRegID, and visitCode are set', () => { + component.visitID = '100'; + component.beneficiaryRegID = '300'; + component.visitCode = '200'; + mockMasterDataService.getLabRequirements.and.returnValue(of({})); + + component.getTestRequirements(); + + expect(mockMasterDataService.getLabRequirements).toHaveBeenCalledWith( + '300', + '100', + '200' + ); + }); + + it('should call errorLoading when visitID is missing', () => { + component.visitID = null; + component.beneficiaryRegID = '300'; + component.visitCode = '200'; + spyOn(component, 'errorLoading'); + + component.getTestRequirements(); + + expect(component.errorLoading).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + + it('should call errorLoading when beneficiaryRegID is missing', () => { + component.visitID = '100'; + component.beneficiaryRegID = null; + component.visitCode = '200'; + spyOn(component, 'errorLoading'); + + component.getTestRequirements(); + + expect(component.errorLoading).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + + it('should call errorLoading when visitCode is missing', () => { + component.visitID = '100'; + component.beneficiaryRegID = '300'; + component.visitCode = null; + spyOn(component, 'errorLoading'); + + component.getTestRequirements(); + + expect(component.errorLoading).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + }); + + describe('loadTests', () => { + it('should call errorLoading when laboratoryList is missing', () => { + spyOn(component, 'errorLoading'); + component.loadTests({ radiologyList: [] }); + + expect(component.errorLoading).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + + it('should call errorLoading when radiologyList is missing', () => { + spyOn(component, 'errorLoading'); + component.loadTests({ laboratoryList: [] }); + + expect(component.errorLoading).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + + it('should call loadlabTests, loadRadiologyTests, loadExternalTests, loadArchive, and mergeForms when data is valid', () => { + spyOn(component, 'loadlabTests'); + spyOn(component, 'loadRadiologyTests'); + spyOn(component, 'loadExternalTests'); + spyOn(component, 'loadArchive'); + spyOn(component, 'mergeForms'); + + const tests = { + laboratoryList: [{ procedureName: 'Lab1' }], + radiologyList: [{ procedureName: 'Rad1' }], + externalTests: { tests: [] }, + archive: [], + }; + + component.loadTests(tests); + + expect(component.loadlabTests).toHaveBeenCalled(); + expect(component.loadRadiologyTests).toHaveBeenCalled(); + expect(component.loadExternalTests).toHaveBeenCalled(); + expect(component.loadArchive).toHaveBeenCalled(); + expect(component.mergeForms).toHaveBeenCalled(); + }); + }); + + describe('component properties', () => { + it('should have valid_file_extensions containing expected types', () => { + expect(component.valid_file_extensions).toContain('pdf'); + expect(component.valid_file_extensions).toContain('png'); + expect(component.valid_file_extensions).toContain('jpeg'); + expect(component.valid_file_extensions).toContain('jpg'); + expect(component.valid_file_extensions).toContain('doc'); + expect(component.valid_file_extensions).toContain('docx'); + expect(component.valid_file_extensions).toContain('xlsx'); + expect(component.valid_file_extensions).toContain('xls'); + expect(component.valid_file_extensions).toContain('csv'); + expect(component.valid_file_extensions).toContain('txt'); + expect(component.valid_file_extensions).toContain('msg'); + }); + + it('should have maxFileSize set to 5', () => { + expect(component.maxFileSize).toBe(5); + }); + + it('should have enableEcgAbnormal initialized to false', () => { + expect(component.enableEcgAbnormal).toBe(false); + }); + }); + + describe('full form lifecycle', () => { + const lang = { + alerts: { + info: { + valueDetails: 'Value should be between', + resetDetails: 'reset?', + selectNewFile: 'select new file', + successMsg: 'success', + uploadSelectedFile: 'upload selected file', + confirmSubmit: 'Do you want to', + labObservation: 'lab observation', + navigateFurtherAlert: 'navigate?', + }, + }, + common: { submit: 'submit' }, + invalidFileExtensionSupportedFileFormats: 'bad ext', + fileSizeShouldNotExceed: 'size exceeds', + mb: 'MB', + invalidFileName: 'bad name', + }; + const labData = () => ({ + laboratoryList: [ + { + procedureType: 'Laboratory', + procedureName: 'ECG', + procedureID: 1, + prescriptionID: 11, + compListDetails: [ + { + inputType: 'TextBox', + range_min: 10, + range_max: 100, + range_normal_min: 20, + range_normal_max: 80, + isDecimal: true, + testComponentID: 101, + testComponentName: 'ECG Rate', + componentCode: 'C1', + }, + { + inputType: 'RadioButton', + testComponentID: 102, + testComponentName: 'ECG Result', + componentCode: 'C2', + compOpt: [{ name: 'Normal' }, { name: 'Abnormal' }], + ecgAbnormalities: ['x'], + }, + ], + }, + { + procedureType: 'Laboratory', + procedureName: 'Sugar', + procedureID: 2, + compListDetails: [ + { + inputType: 'TextBox', + isDecimal: false, + testComponentID: 201, + testComponentName: 'RBS', + componentCode: 'C3', + }, + { + inputType: 'DropDown', + testComponentID: 202, + testComponentName: 'Type', + componentCode: 'C4', + compOpt: [{ name: 'A' }], + }, + ], + }, + ], + radiologyList: [ + { + procedureType: 'Radiology', + procedureName: 'XRay', + procedureID: 'R1', + prescriptionID: 33, + compDetails: { inputType: 'File', testComponentID: 301 }, + }, + { + procedureType: 'Radiology', + procedureName: 'CT', + procedureID: 'R2', + compDetails: { inputType: 'File', testComponentID: 302 }, + }, + ], + externalTests: { tests: ['ext1'] }, + archive: [ + { procedureType: 'Radiology', procedureName: 'XRay' }, + { procedureType: 'Laboratory', procedureName: 'CBC' }, + ], + }); + + let store: Record; + + beforeEach(() => { + store = { + visitID: '100', + visitCode: '200', + beneficiaryRegID: '300', + userID: '7', + userName: 'tech', + serviceLineDetails: JSON.stringify({ vanID: 5, parkingPlaceID: 6 }), + specialist_flag: null, + }; + mockSessionStorageService.getItem.and.callFake((k: string) => + k in store ? store[k] : null + ); + spyOn(console, 'log'); + component.current_language_set = lang; + component.loadTests(labData()); + }); + + it('builds lab, radiology and external forms from the test data', () => { + expect(component.labForm.length).toBe(2); + expect(component.radiologyForm.length).toBe(2); + expect(component.externalForm.value.tests).toEqual(['ext1']); + const comp = (component.labForm.at(0) as FormGroup).controls[ + 'compListDetails' + ] as FormArray; + expect(comp.at(0).value.allowText).toBe('decimal'); + expect( + ( + (component.labForm.at(1) as FormGroup).controls[ + 'compListDetails' + ] as FormArray + ).at(0).value.allowText + ).toBe('number'); + expect((comp.at(1) as FormGroup).value.compOpt.length).toBe(2); + expect( + component.radiologyForm.at(0).value.compDetails.testComponentID + ).toBe(301); + expect(component.labTechnicianData().length).toBe(2); + expect(component.radiologyFormData().length).toBe(2); + expect(component.externalFormData()).toBeDefined(); + expect(component.filteredRadiologyData.length).toBe(1); + expect(component.filteredLaboratoryData.length).toBe(1); + }); + + it('skips building lab/radiology forms for empty lists', () => { + component.labForm = undefined as any; + component.radiologyForm = undefined as any; + component.loadlabTests([]); + component.loadRadiologyTests([]); + component.loadExternalTests(undefined); + expect(component.labForm).toBeUndefined(); + expect(component.radiologyForm).toBeUndefined(); + }); + + it('checkNormalRange flags abnormal and normal values', () => { + const comp = (component.labForm.at(0) as FormGroup).controls[ + 'compListDetails' + ] as FormArray; + comp.at(0).patchValue({ inputValue: 90 }); + component.checkNormalRange(0, 0); + expect(comp.at(0).value.abnormal).toBe(true); + comp.at(0).patchValue({ inputValue: 5 }); + component.checkNormalRange(0, 0); + expect(comp.at(0).value.abnormal).toBe(true); + comp.at(0).patchValue({ inputValue: 50 }); + component.checkNormalRange(0, 0); + expect(comp.at(0).value.abnormal).toBe(false); + comp.at(0).patchValue({ inputValue: null, abnormal: null }); + component.checkNormalRange(0, 0); + expect(comp.at(0).value.abnormal).toBeNull(); + }); + + it('checkRange clears out-of-range values with an alert', () => { + const comp = (component.labForm.at(0) as FormGroup).controls[ + 'compListDetails' + ] as FormArray; + comp.at(0).patchValue({ inputValue: 500 }); + component.checkRange(0, 0); + expect(comp.at(0).value.inputValue).toBe(''); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'Value should be between 10 to 100' + ); + confirmationService.alert.calls.reset(); + comp.at(0).patchValue({ inputValue: 50 }); + component.checkRange(0, 0); + expect(comp.at(0).value.inputValue).toBe(50); + expect(confirmationService.alert).not.toHaveBeenCalled(); + }); + + it('formReCall resets form and reloads requirements', () => { + mockMasterDataService.getLabRequirements.and.returnValue( + of({ statusCode: 200, data: labData() }) + ); + component.visitID = '1'; + component.visitCode = '2'; + component.beneficiaryRegID = '3'; + component.stripSelected = false; + component.formReCall(); + expect(component.stripSelected).toBe(true); + expect(mockMasterDataService.getLabRequirements).toHaveBeenCalledWith( + '3', + '1', + '2' + ); + expect(component.technicianForm).toBeDefined(); + }); + + it('getTestRequirements does nothing on non-200 and alerts on error', () => { + component.visitID = '1'; + component.visitCode = '2'; + component.beneficiaryRegID = '3'; + spyOn(component, 'loadTests'); + mockMasterDataService.getLabRequirements.and.returnValue( + of({ statusCode: 500 }) + ); + component.getTestRequirements(); + expect(component.loadTests).not.toHaveBeenCalled(); + mockMasterDataService.getLabRequirements.and.returnValue( + throwError(() => 'err') + ); + component.getTestRequirements(); + expect(confirmationService.alert).toHaveBeenCalledWith( + component.loadingErrorMessage + ); + }); + + it('ngDoCheck assigns current language', () => { + component.current_language_set = undefined; + component.ngDoCheck(); + expect(component.current_language_set).toEqual({}); + }); + + describe('uploadFile', () => { + const ev = (name: string, size = 1000) => ({ + target: { files: [{ name, size }] }, + }); + + it('does nothing when no files are chosen', () => { + component.uploadFile({ target: { files: [] } }, 1); + expect(component.fileIndex).toBe(1); + expect(confirmationService.alert).not.toHaveBeenCalled(); + }); + + it('alerts for an invalid extension', () => { + component.uploadFile(ev('a.exe'), 1); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'bad ext', + 'error' + ); + }); + + it('alerts when file is too large', () => { + component.uploadFile(ev('a.pdf', 6 * 1000 * 1000), 1); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'size exceeds 5 MB', + 'error' + ); + }); + + it('alerts for an empty file name', () => { + component.uploadFile(ev('.pdf'), 1); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'bad name', + 'error' + ); + }); + + it('reads a valid file via FileReader', () => { + const readSpy = spyOn(FileReader.prototype, 'readAsDataURL').and.stub(); + component.uploadFile(ev('a.pdf'), 1); + expect(readSpy).toHaveBeenCalled(); + }); + + it('onLoadFileCallback assigns the file content', () => { + component.file = { name: 'a.pdf' }; + component.fileIndex = 'R1'; + component.onLoadFileCallback({ + currentTarget: { result: 'data:x;base64,QUJD' }, + }); + expect(component.fileObj['R1'][0]).toEqual( + jasmine.objectContaining({ + fileName: 'a.pdf', + fileExtension: '.pdf', + fileContent: 'QUJD', + userID: '7', + vanID: 5, + isUploaded: false, + }) + ); + }); + }); + + describe('assignFileObject', () => { + it('creates, appends and adds new indexes', () => { + component.file = { name: 'a.pdf' }; + component.assignFileObject('R1', 'h,c1'); + component.assignFileObject('R1', 'h,c2'); + component.assignFileObject('R2', 'h,c3'); + expect(component.fileObj['R1'].length).toBe(2); + expect(component.fileObj['R2'].length).toBe(1); + }); + + it('returns true when the stored entry has the same fileName', () => { + component.file = { name: 'a.pdf' }; + component.fileObj = { R1: { fileName: 'a.pdf' } }; + expect(component.assignFileObject('R1', 'h,c')).toBe(true); + }); + + it('uses empty defaults when no file/content', () => { + component.file = undefined; + store['serviceLineDetails'] = null; + const res: any = component.assignFileObject('R1', undefined); + expect(res.fileName).toBe(''); + expect(res.fileExtension).toBe(''); + expect(res.fileContent).toBe(''); + expect(res.vanID).toBeUndefined(); + }); + }); + + describe('openToViewFile', () => { + it('updates fileObj and removes empty entries after close', () => { + mockDialog.open.and.returnValue({ + afterClosed: () => of({ R1: [], R2: [{}] }), + } as any); + component.openToViewFile('R1'); + expect(component.fileObj).toEqual({ R2: [{}] }); + }); + + it('keeps non-empty entries', () => { + mockDialog.open.and.returnValue({ + afterClosed: () => of({ R1: [{}] }), + } as any); + component.openToViewFile('R1'); + expect(component.fileObj).toEqual({ R1: [{}] }); + }); + }); + + describe('saveUploadDetails', () => { + it('alerts when no fileObj', () => { + component.fileObj = undefined; + component.saveUploadDetails('R1'); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'select new file', + 'info' + ); + }); + + it('alerts when fileObj has no literal procedureID key', () => { + component.fileObj = { R1: [{}] }; + component.saveUploadDetails('R1'); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'select new file', + 'info' + ); + }); + + it('saves when fileObj has literal procedureID key (current behaviour)', () => { + spyOn(component, 'saveFileData'); + component.fileObj = { procedureID: [{ isUploaded: false }] }; + component.saveUploadDetails('procedureID'); + expect(component.saveFileData).toHaveBeenCalledWith('procedureID', [ + { isUploaded: false }, + ]); + }); + + it('saves only new files when saved data exists', () => { + spyOn(component, 'saveFileData'); + component.fileObj = { + procedureID: [{ isUploaded: true }, { isUploaded: false }], + }; + component.savedFileData = { procedureID: [{ isUploaded: true }] }; + component.saveUploadDetails('procedureID'); + expect(component.saveFileData).toHaveBeenCalledWith('procedureID', [ + { isUploaded: false }, + ]); + }); + + it('alerts when no new files among more files', () => { + component.fileObj = { + procedureID: [{ isUploaded: true }, { isUploaded: true }], + }; + component.savedFileData = { procedureID: [{ isUploaded: true }] }; + component.saveUploadDetails('procedureID'); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'select new file', + 'info' + ); + }); + + it('alerts when file count is not larger than saved', () => { + component.fileObj = { procedureID: [{ isUploaded: true }] }; + component.savedFileData = { procedureID: [{ isUploaded: true }] }; + component.saveUploadDetails('procedureID'); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'select new file', + 'info' + ); + }); + }); + + describe('saveFileData', () => { + it('stores saved files and marks uploaded on success', () => { + mockLabService.saveFile.and.returnValue( + of({ + statusCode: 200, + data: [{ filePath: 'f1' }, { filePath: 'f2' }], + }) + ); + component.fileObj = { R1: [{ isUploaded: false }] }; + component.saveFileData('R1', []); + expect(component.savedFileData['R1'].length).toBe(2); + expect(component.savedFileData['R1'][0].isUploaded).toBe(true); + expect(component.fileObj['R1'][0].isUploaded).toBe(true); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'success', + 'success' + ); + mockLabService.saveFile.and.returnValue( + of({ statusCode: 200, data: [{ filePath: 'f3' }] }) + ); + component.saveFileData('R2', []); + expect(component.savedFileData['R2'].length).toBe(1); + component.saveFileData('R2', []); + expect(component.savedFileData['R2'].length).toBe(2); + }); + + it('does nothing on non-200', () => { + mockLabService.saveFile.and.returnValue(of({ statusCode: 500 })); + component.saveFileData('R1', []); + expect(component.savedFileData).toBeUndefined(); + expect(confirmationService.alert).not.toHaveBeenCalled(); + }); + + it('alerts on error', () => { + mockLabService.saveFile.and.returnValue( + throwError(() => ({ errorMessage: 'boom' })) + ); + component.saveFileData('R1', []); + expect(confirmationService.alert).toHaveBeenCalledWith('boom', 'err'); + }); + }); + + describe('validateSubmit', () => { + beforeEach(() => spyOn(component, 'submitDetails')); + + it('submits directly when no files', () => { + component.fileObj = undefined; + component.validateSubmit(true); + expect(component.submitDetails).toHaveBeenCalledWith(true); + }); + + it('alerts when files are not saved', () => { + component.fileObj = { R1: [{}] }; + component.validateSubmit(true); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'upload selected file' + ); + }); + + it('alerts when key counts differ', () => { + component.fileObj = { R1: [{}], R2: [{}] }; + component.savedFileData = { R1: [{}] }; + component.validateSubmit(true); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'upload selected file' + ); + }); + + it('alerts when a key is missing in saved data', () => { + component.fileObj = { R1: [{}] }; + component.savedFileData = { R3: [{}] }; + component.validateSubmit(true); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'upload selected file' + ); + }); + + it('alerts when lengths differ', () => { + component.fileObj = { R1: [{}, {}] }; + component.savedFileData = { R1: [{}] }; + component.validateSubmit(true); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'upload selected file' + ); + expect(component.submitDetails).not.toHaveBeenCalled(); + }); + + it('submits when everything is uploaded', () => { + component.fileObj = { R1: [{}], R2: [{}] }; + component.savedFileData = { R1: [{}], R2: [{}] }; + component.validateSubmit(false); + expect(component.submitDetails).toHaveBeenCalledWith(false); + }); + }); + + describe('submitDetails', () => { + let removeSpy: jasmine.Spy; + beforeEach(() => { + removeSpy = spyOn(sessionStorage, 'removeItem'); + }); + + it('does nothing when the user cancels', () => { + confirmationService.confirm.and.returnValue(of(false)); + component.submitDetails(true); + expect(confirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Do you want to submit lab observation' + ); + expect(mockLabService.saveLabWork).not.toHaveBeenCalled(); + }); + + it('saves lab work with radiology results and navigates on success', () => { + confirmationService.confirm.and.returnValue(of(true)); + mockLabService.saveLabWork.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + component.savedFileData = { R1: [{ filePath: 'p1' }] }; + component.submitDetails(false); + expect(confirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Do you want to save lab observation' + ); + const payload = mockLabService.saveLabWork.calls.mostRecent() + .args[0] as any; + expect(payload.labCompleted).toBe(false); + expect(payload.createdBy).toBe('tech'); + expect(payload.vanID).toBe(5); + expect(payload.parkingPlaceID).toBe(6); + expect(payload.specialist_flag).toBeNull(); + expect(payload.radiologyTestResults).toEqual([ + jasmine.objectContaining({ procedureID: 'R1', fileIDs: ['p1'] }), + ]); + expect(confirmationService.alert).toHaveBeenCalledWith( + 'saved', + 'success' + ); + expect(removeSpy).toHaveBeenCalledWith('visitCode'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/lab/worklist']); + }); + + it('passes specialist_flag and alerts on failure', () => { + store['specialist_flag'] = '1'; + confirmationService.confirm.and.returnValue(of(true)); + mockLabService.saveLabWork.and.returnValue( + of({ statusCode: 500, errorMessage: 'nope' }) + ); + component.submitDetails(true); + const payload = mockLabService.saveLabWork.calls.mostRecent() + .args[0] as any; + expect(payload.specialist_flag).toBe('1'); + expect(confirmationService.alert).toHaveBeenCalledWith('nope', 'error'); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('viewFileContent', () => { + it('downloads the chosen file', () => { + mockDialog.open.and.returnValue({ + afterClosed: () => of({ fileName: 'x.pdf' }), + } as any); + mockLabService.viewFileContent.and.returnValue( + of(new Blob(['a'], { type: 'application/pdf' })) + ); + spyOn(window.URL, 'createObjectURL').and.returnValue('blob:x'); + const clickSpy = spyOn(HTMLAnchorElement.prototype, 'click').and.stub(); + component.viewFileContent([1]); + expect(mockLabService.viewFileContent).toHaveBeenCalledWith({ + fileName: 'x.pdf', + }); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('does nothing when dialog closes empty', () => { + mockDialog.open.and.returnValue({ + afterClosed: () => of(null), + } as any); + component.viewFileContent([1]); + expect(mockLabService.viewFileContent).not.toHaveBeenCalled(); + }); + }); + + describe('openIOTModal', () => { + it('patches textbox and radio values from the device result', () => { + const api = component.labForm.at(0) as FormGroup; + mockDialog.open.and.returnValue({ + afterClosed: () => of(['55', 'Abnormal']), + } as any); + component.openIOTModal(api, 0); + expect(component.stepExpand).toBe(0); + const comp = api.controls['compListDetails'] as FormArray; + expect(comp.at(0).value.inputValue).toBe('55'); + expect(comp.at(1).value.compOptSelected).toBe('Abnormal'); + expect( + (mockDialog.open.calls.mostRecent().args[1] as any).data.output + ).toEqual(['C1', 'C2']); + }); + + it('skips undefined device results', () => { + const api = component.labForm.at(1) as FormGroup; + mockDialog.open.and.returnValue({ + afterClosed: () => of([undefined, 'A']), + } as any); + component.openIOTModal(api, 1); + const comp = api.controls['compListDetails'] as FormArray; + expect(comp.at(0).value.inputValue).toBeNull(); + expect(comp.at(1).value.compOptSelected).toBe('A'); + }); + }); + + it('onStripsCheckBox toggles stripSelected and clears value', () => { + const comp = (component.labForm.at(0) as FormGroup).controls[ + 'compListDetails' + ] as FormArray; + comp.at(0).patchValue({ inputValue: 3 }); + component.onStripsCheckBox({ checked: true }, 0, 0); + expect(component.stripSelected).toBe(false); + expect(comp.at(0).value.inputValue).toBe(''); + component.onStripsCheckBox({ checked: false }, 0, 0); + expect(component.stripSelected).toBe(true); + }); + + it('getEcgAbnormalities logs on error', () => { + mockLabService.getEcgAbnormalities.and.returnValue(throwError(() => 'e')); + component.getEcgAbnormalities(); + expect(console.log).toHaveBeenCalledWith('e', 'error'); + }); + + it('checkForEcg clears ecg abnormalities for matching procedure', () => { + component.checkForEcg('Normal', 'ECG', 0); + expect(component.enableEcgAbnormal).toBe(false); + expect( + component.technicianForm.controls['labForm'].value[0].compListDetails[0] + .ecgAbnormalities + ).toEqual([]); + }); + }); }); diff --git a/src/app/app-modules/lab/worklist/worklist.component.spec.ts b/src/app/app-modules/lab/worklist/worklist.component.spec.ts index 665ecbf3..301dbdb1 100644 --- a/src/app/app-modules/lab/worklist/worklist.component.spec.ts +++ b/src/app/app-modules/lab/worklist/worklist.component.spec.ts @@ -20,17 +20,128 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTableModule } from '@angular/material/table'; +import { of, throwError } from 'rxjs'; import { WorklistComponent } from './worklist.component'; +import { CameraService } from '../../core/services/camera.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { LabService, MasterDataService } from '../shared/services'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('WorklistComponent', () => { let component: WorklistComponent; let fixture: ComponentFixture; + let mockDialog: jasmine.SpyObj; + let mockCameraService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockMasterDataService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockLabService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + let mockHttpServiceService: jasmine.SpyObj; + + const mockWorklistResponse = { + statusCode: 200, + data: [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 'REG001', + benName: 'John Doe', + genderName: 'Male', + age: '30', + VisitCategory: 'General OPD', + benVisitNo: '1', + districtName: 'District A', + villageName: 'Village A', + preferredPhoneNum: '9876543210', + benFlowID: 'FLOW001', + benVisitID: 'VISIT001', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + doctorFlag: '1', + nurseFlag: '9', + specialist_flag: 0, + visitCode: 'VC001', + }, + { + beneficiaryID: 'BEN002', + beneficiaryRegID: 'REG002', + benName: 'Jane Smith', + genderName: 'Female', + age: '25', + VisitCategory: 'ANC', + benVisitNo: '2', + districtName: 'District B', + villageName: 'Village B', + preferredPhoneNum: '9876543211', + benFlowID: 'FLOW002', + benVisitID: 'VISIT002', + visitDate: '2024-01-16T11:00:00', + benVisitDate: '2024-01-16T11:00:00', + doctorFlag: '2', + nurseFlag: '9', + specialist_flag: 1, + visitCode: 'VC002', + }, + ], + }; + + beforeEach(waitForAsync(() => { + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockCameraService = jasmine.createSpyObj('CameraService', ['viewImage']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockMasterDataService = jasmine.createSpyObj('MasterDataService', [ + 'getMasterData', + ]); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['reset', 'getBeneficiaryImage'] + ); + mockLabService = jasmine.createSpyObj('LabService', ['getLabWorklist']); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockHttpServiceService = jasmine.createSpyObj('HttpServiceService', [ + 'setLanguage', + 'currentLangugae$', + ]); + + mockLabService.getLabWorklist.and.returnValue(of(mockWorklistResponse)); + mockHttpServiceService.currentLangugae$ = of({ languageID: 1 }); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [WorklistComponent], + imports: [NoopAnimationsModule, MatPaginatorModule, MatTableModule], + providers: [ + { provide: MatDialog, useValue: mockDialog }, + { provide: CameraService, useValue: mockCameraService }, + { provide: Router, useValue: mockRouter }, + { provide: MasterDataService, useValue: mockMasterDataService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { provide: LabService, useValue: mockLabService }, + { provide: SessionStorageService, useValue: mockSessionStorageService }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -43,4 +154,482 @@ describe('WorklistComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should set currentRole to Lab Technician', () => { + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'currentRole', + 'Lab Technician' + ); + }); + + it('should call loadWorklist', () => { + expect(mockLabService.getLabWorklist).toHaveBeenCalled(); + }); + + it('should call beneficiaryDetailsService.reset', () => { + expect(mockBeneficiaryDetailsService.reset).toHaveBeenCalled(); + }); + }); + + describe('removeBeneficiaryDataForVisit', () => { + it('should remove all expected sessionStorage items', () => { + spyOn(sessionStorage, 'removeItem'); + component.removeBeneficiaryDataForVisit(); + + const expectedKeys = [ + 'visitCode', + 'beneficiaryGender', + 'benFlowID', + 'visitCategory', + 'beneficiaryRegID', + 'visitID', + 'beneficiaryID', + 'doctorFlag', + 'nurseFlag', + 'pharmacist_flag', + 'caseSheetTMFlag', + ]; + + expectedKeys.forEach(key => { + expect(sessionStorage.removeItem).toHaveBeenCalledWith(key); + }); + }); + }); + + describe('ngOnDestroy', () => { + it('should remove currentRole from sessionStorage', () => { + spyOn(sessionStorage, 'removeItem'); + component.ngOnDestroy(); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('currentRole'); + }); + }); + + describe('loadWorklist', () => { + it('should populate beneficiaryList on successful response', () => { + component.loadWorklist(); + expect(component.beneficiaryList).toBeDefined(); + expect(component.beneficiaryList.length).toBe(2); + }); + + it('should populate filteredBeneficiaryList on successful response', () => { + component.loadWorklist(); + expect(component.filteredBeneficiaryList).toBeDefined(); + expect(component.filteredBeneficiaryList.length).toBe(2); + }); + + it('should set filterTerm to null on success', () => { + component.filterTerm = 'test'; + component.loadWorklist(); + expect(component.filterTerm).toBeNull(); + }); + + it('should assign sno to each item in dataSource', () => { + component.loadWorklist(); + expect(component.dataSource.data[0].sno).toBe(1); + expect(component.dataSource.data[1].sno).toBe(2); + }); + + it('should show error alert when statusCode is not 200', () => { + const errorResponse = { + statusCode: 500, + errorMessage: 'Server Error', + }; + mockLabService.getLabWorklist.and.returnValue(of(errorResponse)); + component.loadWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server Error', + 'error' + ); + }); + + it('should clear dataSource data when statusCode is not 200', () => { + const errorResponse = { + statusCode: 500, + errorMessage: 'Server Error', + }; + mockLabService.getLabWorklist.and.returnValue(of(errorResponse)); + component.loadWorklist(); + expect(component.dataSource.data).toEqual([]); + }); + + it('should show error alert on HTTP error', () => { + mockLabService.getLabWorklist.and.returnValue( + throwError('Network error') + ); + component.loadWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + }); + + describe('loadDataToBenList', () => { + it('should map raw data to structured beneficiary list', () => { + const rawData = [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 'REG001', + benName: 'John Doe', + genderName: 'Male', + age: '30', + VisitCategory: 'General OPD', + benVisitNo: '1', + districtName: 'District A', + villageName: 'Village A', + preferredPhoneNum: '9876543210', + benFlowID: 'FLOW001', + benVisitID: 'VISIT001', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + }, + ]; + + const result = component.loadDataToBenList(rawData); + + expect(result.length).toBe(1); + expect(result[0].beneficiaryID).toBe('BEN001'); + expect(result[0].beneficiaryRegID).toBe('REG001'); + expect(result[0].benName).toBe('John Doe'); + expect(result[0].genderName).toBe('Male'); + expect(result[0].age).toBe('30'); + expect(result[0].VisitCategory).toBe('General OPD'); + expect(result[0].districtName).toBe('District A'); + expect(result[0].preferredPhoneNum).toBe('9876543210'); + expect(result[0].benFlowID).toBe('FLOW001'); + expect(result[0].benVisitID).toBe('VISIT001'); + }); + + it('should default missing fields to Not Available', () => { + const rawData = [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 'REG001', + benName: 'Test', + benFlowID: 'FLOW001', + benVisitID: 'VISIT001', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + }, + ]; + + const result = component.loadDataToBenList(rawData); + + expect(result[0].genderName).toBe('Not Available'); + expect(result[0].age).toBe('Not Available'); + expect(result[0].VisitCategory).toBe('Not Available'); + expect(result[0].districtName).toBe('Not Available'); + expect(result[0].villageName).toBe('Not Available'); + expect(result[0].preferredPhoneNum).toBe('Not Available'); + }); + + it('should include labObject reference to original element', () => { + const rawData = [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 'REG001', + benName: 'Test', + benFlowID: 'FLOW001', + benVisitID: 'VISIT001', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + doctorFlag: '1', + nurseFlag: '9', + }, + ]; + + const result = component.loadDataToBenList(rawData); + + expect(result[0].labObject).toBe(rawData[0]); + }); + + it('should return empty array for empty input', () => { + const result = component.loadDataToBenList([]); + expect(result).toEqual([]); + }); + }); + + describe('filterBeneficiaryList', () => { + beforeEach(() => { + component.beneficiaryList = [ + { + beneficiaryID: 'BEN001', + benName: 'John Doe', + genderName: 'Male', + age: '30', + VisitCategory: 'General OPD', + benVisitNo: '1', + districtName: 'District A', + preferredPhoneNum: '9876543210', + villageName: 'Village A', + beneficiaryRegID: 'REG001', + visitDate: '15-01-2024 10:30 AM', + }, + { + beneficiaryID: 'BEN002', + benName: 'Jane Smith', + genderName: 'Female', + age: '25', + VisitCategory: 'ANC', + benVisitNo: '2', + districtName: 'District B', + preferredPhoneNum: '9876543211', + villageName: 'Village B', + beneficiaryRegID: 'REG002', + visitDate: '16-01-2024 11:00 AM', + }, + ]; + }); + + it('should reset filteredBeneficiaryList when searchTerm is empty', () => { + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + }); + + it('should filter by beneficiaryID', () => { + component.filterBeneficiaryList('BEN001'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe('BEN001'); + }); + + it('should filter by benName', () => { + component.filterBeneficiaryList('Jane'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('Jane Smith'); + }); + + it('should filter case-insensitively', () => { + component.filterBeneficiaryList('john'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('John Doe'); + }); + + it('should filter by districtName', () => { + component.filterBeneficiaryList('District B'); + expect(component.filteredBeneficiaryList.length).toBe(1); + }); + + it('should filter by preferredPhoneNum', () => { + component.filterBeneficiaryList('9876543211'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe('BEN002'); + }); + + it('should return empty when no match', () => { + component.filterBeneficiaryList('NONEXISTENT'); + expect(component.filteredBeneficiaryList.length).toBe(0); + }); + + it('should assign sno to filtered results', () => { + component.filterBeneficiaryList('BEN'); + expect(component.dataSource.data.length).toBe(2); + }); + }); + + describe('patientImageView', () => { + it('should call getBeneficiaryImage when benregID is valid', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'base64imagedata' }) + ); + component.patientImageView('REG001'); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).toHaveBeenCalledWith('REG001'); + }); + + it('should call cameraService.viewImage when image is found', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'base64imagedata' }) + ); + component.patientImageView('REG001'); + expect(mockCameraService.viewImage).toHaveBeenCalledWith( + 'base64imagedata' + ); + }); + + it('should show alert when image is not found', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: null }) + ); + component.current_language_set = { + alerts: { info: { imageNotFound: 'Image not found' } }, + }; + component.patientImageView('REG001'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Image not found' + ); + }); + + it('should not call getBeneficiaryImage when benregID is null', () => { + component.patientImageView(null); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + + it('should not call getBeneficiaryImage when benregID is empty string', () => { + component.patientImageView(''); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + + it('should not call getBeneficiaryImage when benregID is undefined', () => { + component.patientImageView(undefined); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + }); + + describe('loadLabExaminationPage', () => { + const mockBeneficiary = { + beneficiaryID: 'BEN001', + beneficiaryRegID: 'REG001', + benVisitID: 'VISIT001', + VisitCategory: 'General OPD', + benFlowID: 'FLOW001', + labObject: { + doctorFlag: '1', + nurseFlag: '9', + visitCode: 'VC001', + specialist_flag: 0, + }, + }; + + it('should call confirmationService.confirm', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.current_language_set = { + alerts: { info: { confirmtoProceedFurther: 'Confirm to proceed' } }, + }; + component.loadLabExaminationPage(mockBeneficiary); + expect(mockConfirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Confirm to proceed' + ); + }); + + it('should set sessionStorage items and navigate on confirm', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.current_language_set = { + alerts: { info: { confirmtoProceedFurther: 'Confirm to proceed' } }, + }; + component.loadLabExaminationPage(mockBeneficiary); + + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'doctorFlag', + '1' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'nurseFlag', + '9' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitID', + 'VISIT001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'beneficiaryRegID', + 'REG001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'beneficiaryID', + 'BEN001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitCategory', + 'General OPD' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'benFlowID', + 'FLOW001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitCode', + 'VC001' + ); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/lab/patient/', + 'REG001', + ]); + }); + + it('should set specialist_flag when present and > 0', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.current_language_set = { + alerts: { info: { confirmtoProceedFurther: 'Confirm' } }, + }; + component.loadLabExaminationPage({ + ...mockBeneficiary, + labObject: { ...mockBeneficiary.labObject, specialist_flag: 2 }, + }); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'specialist_flag', + 2 + ); + }); + + it('should NOT store specialist_flag 0 (falsy check, current behaviour)', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + mockSessionStorageService.getItem.and.returnValue('1'); + component.current_language_set = { + alerts: { info: { confirmtoProceedFurther: 'Confirm' } }, + }; + component.loadLabExaminationPage(mockBeneficiary); + expect(mockSessionStorageService.setItem).not.toHaveBeenCalledWith( + 'specialist_flag', + jasmine.anything() + ); + expect(mockSessionStorageService.getItem).toHaveBeenCalledWith( + 'specialist_flag' + ); + expect(mockRouter.navigate).toHaveBeenCalled(); + }); + + it('should not navigate when user declines confirmation', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.current_language_set = { + alerts: { info: { confirmtoProceedFurther: 'Confirm' } }, + }; + component.loadLabExaminationPage(mockBeneficiary); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('pageChanged', () => { + it('should slice filteredBeneficiaryList based on event', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + const event = { page: 1, itemsPerPage: 2 }; + component.pageChanged(event); + expect(component.pagedList.length).toBe(2); + expect(component.pagedList as any[]).toEqual([{ id: 1 }, { id: 2 }]); + }); + + it('should return correct page for second page', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + const event = { page: 2, itemsPerPage: 2 }; + component.pageChanged(event); + expect(component.pagedList as any[]).toEqual([{ id: 3 }, { id: 4 }]); + }); + + it('should return remaining items on last page', () => { + component.filteredBeneficiaryList = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const event = { page: 2, itemsPerPage: 2 }; + component.pageChanged(event); + expect(component.pagedList as any[]).toEqual([{ id: 3 }]); + }); + }); }); diff --git a/src/app/app-modules/login/login.component.spec.ts b/src/app/app-modules/login/login.component.spec.ts index ccfd09db..f853e1a5 100644 --- a/src/app/app-modules/login/login.component.spec.ts +++ b/src/app/app-modules/login/login.component.spec.ts @@ -19,28 +19,467 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { of, Subject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + commonTestProviders, + autoSpy, + createDialogRefMock, + throwingObs, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; +import { LoginComponent } from './login.component'; +import { AuthService } from 'src/app/app-modules/core/services/auth.service'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { DataSyncLoginComponent } from '../core/components/data-sync-login/data-sync-login.component'; +import { MasterDownloadComponent } from '../data-sync/master-download/master-download.component'; +import { CampHubQrCodeComponent } from '../data-sync/camp-hub-qr-code/camp-hub-qr-code.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +const ALREADY_LOGGED_IN = + 'You are already logged in,please confirm to logout from other device and login again'; -import { LoginComponent } from './login.component'; +function loginData(overrides: any = {}) { + return { + key: 'k1', + isAuthenticated: 'true', + userID: 7, + userName: 'nurse1', + fullName: 'Nurse One', + Status: 'Active', + previlegeObj: [ + { + providerServiceMapID: 11, + serviceID: 101, + serviceName: 'MMU', + apimanClientKey: 'api-key', + roles: [ + { + serviceRoleScreenMappings: [ + { + providerServiceMapping: { + serviceID: 2, + serviceProviderID: 5, + }, + }, + ], + }, + ], + }, + { + providerServiceMapID: 12, + serviceID: 102, + serviceName: 'TM', + apimanClientKey: 'api-key-2', + roles: [ + { + serviceRoleScreenMappings: [ + { + providerServiceMapping: { + serviceID: 4, + serviceProviderID: 5, + }, + }, + ], + }, + ], + }, + ], + ...overrides, + }; +} describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture; + let auth: any; + let confirmation: any; + let session: any; + let tracking: any; + let dialog: any; + let router: Router; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [LoginComponent], - }).compileComponents(); - })); - - beforeEach(() => { + function create() { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture.detectChanges(); + } + + beforeEach(async () => { + sessionStorage.clear(); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [LoginComponent], + providers: [ + ...commonTestProviders(), + { provide: AuthService, useValue: autoSpy(AuthService) }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + auth = TestBed.inject(AuthService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + session = TestBed.inject(SessionStorageService) as any; + tracking = TestBed.inject(AmritTrackingService) as any; + dialog = TestBed.inject(MatDialog) as any; + }); + + afterEach(() => { + sessionStorage.clear(); + }); + + describe('init', () => { + it('creates, sets crypto params and focuses the username field', () => { + create(); + expect(component).toBeTruthy(); + expect(component._keySize).toBe(256); + expect(component._ivSize).toBe(128); + expect(component._iterationCount).toBe(1989); + const input = fixture.nativeElement.querySelector('#userID'); + const focusSpy = spyOn(input, 'focus'); + component.ngAfterViewInit(); + expect(focusSpy).toHaveBeenCalled(); + expect(component.loginForm.valid).toBeFalse(); + }); + + it('clears session storage when not authenticated', () => { + sessionStorage.setItem('other', 'x'); + create(); + expect(sessionStorage.getItem('other')).toBeNull(); + expect(auth.validateSessionKey).not.toHaveBeenCalled(); + }); + + it('validates existing session and navigates to /service on success', () => { + sessionStorage.setItem('isAuthenticated', 'true'); + auth.validateSessionKey.and.returnValue( + of({ statusCode: 200, data: { ok: 1 } }) + ); + create(); + expect(auth.validateSessionKey).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/service']); + }); + + it('does not navigate when session validation fails', () => { + sessionStorage.setItem('isAuthenticated', 'true'); + auth.validateSessionKey.and.returnValue(of({ statusCode: 5002 })); + create(); + expect(router.navigate).not.toHaveBeenCalled(); + auth.validateSessionKey.and.returnValue(of(null)); + component.ngOnInit(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); - it('should be created', () => { - expect(component).toBeTruthy(); + describe('helpers', () => { + beforeEach(() => create()); + + it('toggles password visibility', () => { + component.showPWD(); + expect(component.dynamictype).toBe('text'); + component.hidePWD(); + expect(component.dynamictype).toBe('password'); + }); + + it('exposes keySize and iterationCount getters/setters', () => { + component.keySize = 128; + component.iterationCount = 10; + expect(component.keySize).toBe(128); + expect(component.iterationCount).toBe(10); + expect(component._keySize).toBe(128); + }); + + it('encrypt returns salt(64 hex) + iv(32 hex) + base64 ciphertext', () => { + component.iterationCount = 2; + const out = component.encrypt(component.Key_IV, 'secret'); + expect(out.length).toBeGreaterThan(96); + expect(/^[0-9a-f]{96}/.test(out)).toBeTrue(); + const a = component.encryptWithIvSalt( + 'aa'.repeat(32), + 'bb'.repeat(16), + 'p', + 'x' + ); + const b = component.encryptWithIvSalt( + 'aa'.repeat(32), + 'bb'.repeat(16), + 'p', + 'x' + ); + expect(a).toBe(b); + }); + + it('generateKey uses keySize/32 words', () => { + component.iterationCount = 1; + const key = component.generateKey('aa'.repeat(32), 'p'); + expect(key.sigBytes).toBe(32); + }); + + it('stores captcha token', () => { + component.onCaptchaResolved('tok'); + expect(component.captchaToken).toBe('tok'); + }); + + it('resetCaptcha resets only when enabled and component present', () => { + const reset = jasmine.createSpy('reset'); + component.captchaToken = 'tok'; + component.enableCaptcha = false; + component.captchaCmp = { reset } as any; + component.resetCaptcha(); + expect(reset).not.toHaveBeenCalled(); + + component.enableCaptcha = true; + component.captchaCmp = undefined; + component.resetCaptcha(); + expect(component.captchaToken).toBe('tok'); + + component.captchaCmp = { reset } as any; + component.resetCaptcha(); + expect(reset).toHaveBeenCalled(); + expect(component.captchaToken).toBe(''); + }); + }); + + describe('login', () => { + beforeEach(() => { + create(); + component.iterationCount = 1; + component.enableCaptcha = false; + }); + + function fill(user = ' nurse1 ', pwd = 'pwd') { + component.loginForm.setValue({ userName: user, password: pwd }); + } + + it('does nothing when the form is invalid', () => { + component.login(); + expect(auth.login).not.toHaveBeenCalled(); + }); + + it('logs in with trimmed username and stores data on success', () => { + fill(); + auth.login.and.returnValue(of({ statusCode: 200, data: loginData() })); + component.login(); + const args = auth.login.calls.mostRecent().args; + expect(args[0]).toBe('nurse1'); + expect(args[2]).toBeFalse(); + expect(args[3]).toBeUndefined(); + expect(session.setItem).toHaveBeenCalledWith( + 'loginDataResponse', + jasmine.any(String) + ); + expect(sessionStorage.getItem('key')).toBe('k1'); + expect(sessionStorage.getItem('isAuthenticated')).toBe('true'); + expect(tracking.setUserId).toHaveBeenCalledWith(7); + expect(session.setItem).toHaveBeenCalledWith('providerServiceMapID', 11); + expect(session.setItem).toHaveBeenCalledWith('currentServiceID', 2); + const services = JSON.parse(session.store.get('services')); + expect(services.length).toBe(1); + expect(services[0]).toEqual({ + providerServiceID: 101, + serviceName: 'MMU', + apimanClientKey: 'api-key', + serviceID: 2, + serviceProviderID: 5, + }); + expect(router.navigate).toHaveBeenCalledWith(['/service']); + }); + + it('passes captcha token when captcha is enabled', () => { + component.enableCaptcha = true; + component.captchaToken = 'cap'; + fill(); + auth.login.and.returnValue(of({ statusCode: 200, data: loginData() })); + component.login(); + expect(auth.login.calls.mostRecent().args[3]).toBe('cap'); + }); + + it('routes new users to security questions', () => { + fill(); + auth.login.and.returnValue( + of({ statusCode: 200, data: loginData({ Status: 'New' }) }) + ); + component.login(); + expect(router.navigate).toHaveBeenCalledWith(['/set-security-questions']); + }); + + it('alerts when user has no MMU service privilege', () => { + const data = loginData(); + data.previlegeObj = [data.previlegeObj[1]]; + fill(); + auth.login.and.returnValue(of({ statusCode: 200, data })); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + "User doesn't have previlege to access the application" + ); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts when 200 has no privileges', () => { + fill(); + auth.login.and.returnValue( + of({ statusCode: 200, data: loginData({ previlegeObj: [] }) }) + ); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Seems you are logged in from somewhere else, Logout from there & try back in.', + 'error' + ); + }); + + it('alerts error message for other 5002 errors', () => { + fill(); + auth.login.and.returnValue( + of({ statusCode: 5002, errorMessage: 'Bad credentials' }) + ); + spyOn(component, 'resetCaptcha').and.callThrough(); + component.login(); + expect(component.resetCaptcha).toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Bad credentials', + 'error' + ); + }); + + it('ignores non-200/non-5002 responses', () => { + fill(); + auth.login.and.returnValue(of({ statusCode: 500 })); + component.login(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + describe('already logged in elsewhere', () => { + beforeEach(() => { + fill('nurse1'); + auth.login.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY_LOGGED_IN }), + of({ statusCode: 200, data: loginData() }) + ); + }); + + it('logs out previous session and logs in again on confirm', () => { + auth.userlogoutPreviousSession.and.returnValue(of({ statusCode: 200 })); + component.login(); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + ALREADY_LOGGED_IN + ); + expect(auth.userlogoutPreviousSession).toHaveBeenCalledWith('nurse1'); + expect(auth.login).toHaveBeenCalledTimes(2); + expect(auth.login.calls.mostRecent().args[2]).toBeTrue(); + expect(tracking.setUserId).toHaveBeenCalledWith(7); + expect(router.navigate).toHaveBeenCalledWith(['/service']); + }); + + it('alerts when relogin returns no privileges', () => { + auth.login.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY_LOGGED_IN }), + of({ statusCode: 200, data: loginData({ previlegeObj: [] }) }) + ); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Seems you are logged in from somewhere else, Logout from there & try back in.', + 'error' + ); + }); + + it('alerts relogin error message when relogin fails', () => { + auth.login.and.returnValues( + of({ statusCode: 5002, errorMessage: ALREADY_LOGGED_IN }), + of({ statusCode: 500, errorMessage: 'relogin failed' }) + ); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'relogin failed', + 'error' + ); + }); + + it('alerts when logging out previous session fails', () => { + auth.userlogoutPreviousSession.and.returnValue( + of({ statusCode: 500, errorMessage: 'logout failed' }) + ); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'logout failed', + 'error' + ); + expect(auth.login).toHaveBeenCalledTimes(1); + }); + + it('clears session and goes to /login when user declines', () => { + confirmation.confirm.and.returnValue(of(false)); + sessionStorage.setItem('x', '1'); + component.login(); + expect(auth.userlogoutPreviousSession).not.toHaveBeenCalled(); + expect(sessionStorage.getItem('x')).toBeNull(); + expect(router.navigate).toHaveBeenCalledWith(['/login']); + }); + }); + + it('alerts server error message on http error', () => { + fill(); + auth.login.and.returnValue( + throwingObs({ error: { errorMessage: 'server says no' } }) + ); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'server says no', + 'error' + ); + }); + + it('falls back to err.message, then default message', () => { + fill(); + auth.login.and.returnValue(throwingObs({ message: 'net down' })); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith('net down', 'error'); + auth.login.and.returnValue(throwingObs(null)); + component.login(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Login request failed. Please try again.', + 'error' + ); + }); + }); + + describe('dialogs', () => { + beforeEach(() => create()); + + it('opens the camp hub QR dialog', () => { + component.openQrDialog(); + expect(dialog.open).toHaveBeenCalledWith(CampHubQrCodeComponent, { + width: '500px', + disableClose: false, + }); + }); + + it('opens master download after successful data-sync login', () => { + const first = createDialogRefMock(true); + const second = createDialogRefMock(undefined); + dialog.open.and.returnValues(first, second); + sessionStorage.setItem('x', '1'); + component.openDialog(); + expect(dialog.open.calls.argsFor(0)[0]).toBe(DataSyncLoginComponent); + expect(dialog.open.calls.argsFor(0)[1].data).toEqual({ + masterDowloadFirstTime: true, + }); + expect(dialog.open.calls.argsFor(1)[0]).toBe(MasterDownloadComponent); + expect(sessionStorage.getItem('x')).toBeNull(); + }); + + it('does not open master download when data-sync login is cancelled', () => { + const closed = new Subject(); + const ref = createDialogRefMock(); + ref.afterClosed.and.returnValue(closed.asObservable()); + dialog.open.and.returnValue(ref); + component.openDialog(); + closed.next(false); + expect(dialog.open).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/anc/anc-details/anc-details.component.spec.ts b/src/app/app-modules/nurse-doctor/anc/anc-details/anc-details.component.spec.ts index 70ea7a4e..3cc7fc3d 100644 --- a/src/app/app-modules/nurse-doctor/anc/anc-details/anc-details.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/anc/anc-details/anc-details.component.spec.ts @@ -20,27 +20,143 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; import { AncDetailsComponent } from './anc-details.component'; describe('AncDetailsComponent', () => { let component: AncDetailsComponent; let fixture: ComponentFixture; + let confirm: any; + let ben$: BehaviorSubject; + const DAY = 24 * 60 * 60 * 1000; + + const makeForm = () => + new FormGroup({ + lmpDate: new FormControl(null), + duration: new FormControl(5), + expDelDt: new FormControl(null), + gestationalAgeOrPeriodofAmenorrhea_POA: new FormControl(null), + trimesterNumber: new FormControl(null), + primiGravida: new FormControl(true), + }); + const asMoment = (d: Date) => ({ toDate: () => d }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [AncDetailsComponent], + providers: [ + ...commonTestProviders(), + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(AncDetailsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + component.patientANCDetailsForm = makeForm(); + component.ngOnInit(); + component.ngDoCheck(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('initialises dates, language and tracks beneficiary age', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.today).toBeDefined(); + expect(component.beneficiaryAge).toBeUndefined(); + ben$.next({ ageVal: 24 }); + expect(component.beneficiaryAge).toBe(24); + }); + + it('checkupLMP with a valid date computes EDD, gestational age and trimester', () => { + const lmp = new Date(component.today.getTime() - 70 * DAY); + component.checkupLMP(asMoment(lmp)); + const v = component.patientANCDetailsForm.value; + expect(v.lmpDate).toBe(lmp); + expect(v.duration).toBeNull(); + const edd = new Date(lmp); + edd.setDate(lmp.getDate() + 280); + expect(v.expDelDt).toEqual(edd); + expect(v.gestationalAgeOrPeriodofAmenorrhea_POA).toBe(10); + expect(v.trimesterNumber).toBe(1); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + + it('checkupLMP with an out-of-range date alerts and clears values', () => { + component.patientANCDetailsForm.patchValue({ + trimesterNumber: 2, + expDelDt: 'x', + }); + component.checkupLMP(asMoment(new Date(Date.now() + 5 * DAY))); + const v = component.patientANCDetailsForm.value; + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.invalidVal + ); + expect(v.lmpDate).toBeNull(); + expect(v.expDelDt).toBeNull(); + expect(v.gestationalAgeOrPeriodofAmenorrhea_POA).toBeNull(); + expect(v.trimesterNumber).toBeNull(); + }); + + it('calculateTrimester maps weeks to trimesters', () => { + const t = (w: any) => { + component.calculateTrimester(w); + return component.patientANCDetailsForm.value.trimesterNumber; + }; + expect(t(5)).toBe(1); + expect(t(12)).toBe(2); + expect(t(20)).toBe(2); + expect(t(27)).toBe(3); + expect(t(35)).toBe(3); + expect(t(null)).toBeNull(); + }); + + it('checkPeriodOfPregnancy validates range', () => { + component.checkPeriodOfPregnancy(5); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(component.duration).toBe(5); + component.checkPeriodOfPregnancy(10); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.invalidValue + ); + expect(component.duration).toBeNull(); + component.checkPeriodOfPregnancy(0); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.common.invalidValueMorethan + ); + }); + + it('getters read the form', () => { + component.patientANCDetailsForm.patchValue({ + lmpDate: 'd', + gestationalAgeOrPeriodofAmenorrhea_POA: 8, + }); + expect(component.primiGravida).toBeTrue(); + expect(component.lmpDate).toBe('d'); + expect(component.gestationalAgeOrPeriodofAmenorrhea_POA).toBe(8); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', () => { + const u = spyOn(component.beneficiaryDetailsSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(u).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/anc/anc-immunization/anc-immunization.component.spec.ts b/src/app/app-modules/nurse-doctor/anc/anc-immunization/anc-immunization.component.spec.ts index 77aea601..4b7eb32b 100644 --- a/src/app/app-modules/nurse-doctor/anc/anc-immunization/anc-immunization.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/anc/anc-immunization/anc-immunization.component.spec.ts @@ -20,27 +20,170 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; import { AncImmunizationComponent } from './anc-immunization.component'; describe('AncImmunizationComponent', () => { let component: AncImmunizationComponent; let fixture: ComponentFixture; + let ben$: BehaviorSubject; + + const keys = [ + 'tT_1Status', + 'dateReceivedForTT_1', + 'facilityNameOfTT_1', + 'tT_2Status', + 'dateReceivedForTT_2', + 'facilityNameOfTT_2', + 'tT_3Status', + 'dateReceivedForTT_3', + 'facilityNameOfTT_3', + 'vanID', + 'parkingPlaceID', + ]; + const makeForm = () => { + const g: any = {}; + keys.forEach(k => (g[k] = new FormControl('x'))); + return new FormGroup(g); + }; + const form = () => component.patientANCImmunizationForm; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [AncImmunizationComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 5, parkingPlaceID: 6 }), + }, + }), + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(AncImmunizationComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.patientANCImmunizationForm = makeForm(); + component.ngOnInit(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('beneficiary details set age and dob', () => { + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.dob).toBeUndefined(); + ben$.next({ ageVal: 25 }); + expect(component.beneficiaryAge).toBe(25); + expect(component.dob.getFullYear()).toBe(new Date().getFullYear() - 25); + }); + + it('ngOnChanges in new mode resets form keeping van ids', () => { + component.gravidaStatus = true; + component.ngOnChanges({}); + expect(component.enableTTStatus_1_2).toBeTrue(); + expect(component.enableTTStatus_1_2_b).toBeFalse(); + expect(form().value.tT_1Status).toBeNull(); + expect(form().value.vanID).toBe(5); + expect(form().value.parkingPlaceID).toBe(6); + }); + + it('ngOnChanges in view/update mode keeps values', () => { + for (const mode of ['view', 'UPDATE']) { + component.mode = mode; + component.gravidaStatus = false; + component.ngOnChanges({}); + expect(component.enableTTStatus_1_2_b).toBeTrue(); + expect(component.enableTTStatus_1_2).toBeFalse(); + expect(form().value.tT_1Status).toBe('x'); + } + component.gravidaStatus = null; + component.checkStatus(); + expect(component.enableTTStatus_1_2_b).toBeFalse(); + expect(component.enableTTStatus_1_2).toBeFalse(); + }); + + it('checkTT_1Status clears all dependent fields', () => { + component.checkTT_1Status('Received'); + const v = form().value; + keys + .filter(k => !['tT_1Status', 'vanID', 'parkingPlaceID'].includes(k)) + .forEach(k => expect(v[k]).withContext(k).toBeNull()); + expect(component.tT_1Status).toBe('x'); + expect(component.checkedTT_1Status).toBeFalse(); + }); + + it('checkTT_1Date / checkTT_2Date clear later fields', () => { + component.checkTT_2Date(null); + expect(form().value.facilityNameOfTT_2).toBeNull(); + expect(form().value.tT_2Status).toBe('x'); + component.checkTT_1Date(null); + expect(form().value.tT_2Status).toBeNull(); + expect(form().value.dateReceivedForTT_1).toBe('x'); + }); + + it('checkTT_2Status sets TT1 min date from TT1 date or dob', () => { + ben$.next({ ageVal: 20 }); + component.checkTT_2Status('Received'); + expect(component.tT_1Date).toBe('x'); + expect(form().value.tT_3Status).toBeNull(); + form().patchValue({ dateReceivedForTT_1: null }); + component.checkTT_2Status('Received'); + expect(component.tT_1Date).toBe(component.dob); + component.tT_1Date = 'keep'; + component.checkTT_2Status('Not Received'); + expect(component.tT_1Date).toBe('keep'); + expect(component.tT_2Status).toBe('x'); + }); + + it('checkTT_3Status picks the latest received dose date', () => { + ben$.next({ ageVal: 20 }); + const d1 = new Date(2024, 0, 1); + const d2 = new Date(2024, 1, 1); + form().patchValue({ + tT_1Status: 'Received', + dateReceivedForTT_1: d1, + tT_2Status: 'Received', + dateReceivedForTT_2: d2, + }); + component.checkTT_3Status('Received'); + expect(component.tT_3Date).toBe(d2); + expect(form().value.dateReceivedForTT_3).toBeNull(); + + form().patchValue({ tT_2Status: 'Not Received' }); + component.checkTT_3Status('Received'); + expect(component.tT_3Date).toBe(d1); + + form().patchValue({ tT_1Status: 'NA' }); + component.checkTT_3Status('Received'); + expect(component.tT_3Date).toBe(component.dob); + + component.tT_3Date = 'keep'; + component.checkTT_3Status('NA'); + expect(component.tT_3Date).toBe('keep'); + expect(component.tT_3Status).toBe('x'); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', () => { + const u = spyOn(component.beneficiaryDetailsSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(u).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/anc/anc.component.spec.ts b/src/app/app-modules/nurse-doctor/anc/anc.component.spec.ts index ab16d1f6..dca2f5b5 100644 --- a/src/app/app-modules/nurse-doctor/anc/anc.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/anc/anc.component.spec.ts @@ -20,27 +20,543 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { BehaviorSubject, of, throwError } from 'rxjs'; import { AncComponent } from './anc.component'; +import { DoctorService } from '../shared/services'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../core/services'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('AncComponent', () => { let component: AncComponent; let fixture: ComponentFixture; + let mockDoctorService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + + function buildANCForm(): FormGroup { + return new FormGroup({ + patientANCDetailsForm: new FormGroup({ + primiGravida: new FormControl(false), + lmpDate: new FormControl(null), + expDelDt: new FormControl(null), + }), + obstetricFormulaForm: new FormGroup({ + bloodGroup: new FormControl(''), + }), + patientANCImmunizationForm: new FormGroup({ + dateReceivedForTT_1: new FormControl(null), + dateReceivedForTT_2: new FormControl(null), + dateReceivedForTT_3: new FormControl(null), + }), + }); + } + + beforeEach(waitForAsync(() => { + mockDoctorService = jasmine.createSpyObj('DoctorService', [ + 'getAncCareDetails', + 'updateANCDetails', + 'getHRPDetails', + ]); + mockDoctorService.getAncCareDetails.and.returnValue(of({})); + mockDoctorService.updateANCDetails.and.returnValue(of({})); + mockDoctorService.getHRPDetails.and.returnValue(of({})); + + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['setHRPPositive', 'resetHRPPositive'] + ); + + mockHttpService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: new BehaviorSubject({ test: 'language' }), + }); + + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockSessionStorage.getItem.and.callFake((key: string) => { + const store: Record = { + visitID: '100', + beneficiaryRegID: '200', + beneficiaryID: '300', + sessionID: 'sess-1', + userName: 'testUser', + providerServiceID: 'prov-1', + benFlowID: 'flow-1', + visitCode: 'vc-1', + serviceLineDetails: JSON.stringify({ + vanID: 10, + parkingPlaceID: 20, + }), + }; + return store[key] ?? null; + }); - beforeEach(async(() => { TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], declarations: [AncComponent], + providers: [ + { provide: DoctorService, useValue: mockDoctorService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AncComponent); component = fixture.componentInstance; + component.patientANCDataForm = buildANCForm(); + component.mode = ''; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should extract patientANCDetailsForm from parent form', () => { + expect(component.patientANCDetailsForm).toBeTruthy(); + expect(component.patientANCDetailsForm.get('primiGravida')).toBeTruthy(); + }); + + it('should extract obstetricFormulaForm from parent form', () => { + expect(component.obstetricFormulaForm).toBeTruthy(); + expect(component.obstetricFormulaForm.get('bloodGroup')).toBeTruthy(); + }); + + it('should extract patientANCImmunizationForm from parent form', () => { + expect(component.patientANCImmunizationForm).toBeTruthy(); + expect( + component.patientANCImmunizationForm.get('dateReceivedForTT_1') + ).toBeTruthy(); + }); + + it('should subscribe to primiGravida valueChanges and update gravidaStatus', () => { + const primiControl = ( + component.patientANCDataForm.get('patientANCDetailsForm') as FormGroup + ).controls['primiGravida']; + + primiControl.setValue(true); + expect(component.gravidaStatus).toBeTrue(); + + primiControl.setValue(false); + expect(component.gravidaStatus).toBeFalse(); + }); + }); + + describe('ngDoCheck', () => { + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngDoCheck(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('assignSelectedLanguage', () => { + it('should set current_language_set from the language service', () => { + component.assignSelectedLanguage(); + expect(component.current_language_set).toEqual({ test: 'language' }); + }); + }); + + describe('ngOnChanges', () => { + it('should call patchDataToFields when mode is view', () => { + spyOn(component, 'patchDataToFields' as any); + component.mode = 'view'; + component.ngOnChanges(); + expect((component as any).patchDataToFields).toHaveBeenCalledWith( + '200', + '100' + ); + }); + + it('should call updatePatientANC when mode is update', () => { + spyOn(component, 'updatePatientANC' as any); + component.mode = 'update'; + component.ngOnChanges(); + expect((component as any).updatePatientANC).toHaveBeenCalledWith( + component.patientANCDataForm + ); + }); + + it('should not call patchDataToFields or updatePatientANC when mode is empty', () => { + spyOn(component, 'patchDataToFields' as any); + spyOn(component, 'updatePatientANC' as any); + component.mode = ''; + component.ngOnChanges(); + expect((component as any).patchDataToFields).not.toHaveBeenCalled(); + expect((component as any).updatePatientANC).not.toHaveBeenCalled(); + }); + + it('should not call patchDataToFields or updatePatientANC when mode is null', () => { + spyOn(component, 'patchDataToFields' as any); + spyOn(component, 'updatePatientANC' as any); + component.mode = null as any; + component.ngOnChanges(); + expect((component as any).patchDataToFields).not.toHaveBeenCalled(); + expect((component as any).updatePatientANC).not.toHaveBeenCalled(); + }); + + it('should not call patchDataToFields or updatePatientANC when mode is undefined', () => { + spyOn(component, 'patchDataToFields' as any); + spyOn(component, 'updatePatientANC' as any); + component.mode = undefined as any; + component.ngOnChanges(); + expect((component as any).patchDataToFields).not.toHaveBeenCalled(); + expect((component as any).updatePatientANC).not.toHaveBeenCalled(); + }); + }); + + describe('ngOnDestroy', () => { + it('should unsubscribe ancCareDetails if it exists', () => { + const mockSub = jasmine.createSpyObj('Subscription', ['unsubscribe']); + component.ancCareDetails = mockSub; + component.ngOnDestroy(); + expect(mockSub.unsubscribe).toHaveBeenCalled(); + }); + + it('should unsubscribe updateANCDetailsSubs if it exists', () => { + const mockSub = jasmine.createSpyObj('Subscription', ['unsubscribe']); + component.updateANCDetailsSubs = mockSub; + component.ngOnDestroy(); + expect(mockSub.unsubscribe).toHaveBeenCalled(); + }); + + it('should not throw when subscriptions are undefined', () => { + component.ancCareDetails = undefined; + component.updateANCDetailsSubs = undefined; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('updatePatientANC', () => { + it('should call doctorService.updateANCDetails with form and temp object', () => { + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.updatePatientANC(component.patientANCDataForm); + expect(mockDoctorService.updateANCDetails).toHaveBeenCalled(); + const args = mockDoctorService.updateANCDetails.calls.mostRecent().args; + expect(args[1].beneficiaryRegID).toBe('200'); + expect(args[1].vanID).toBe(10); + expect(args[1].parkingPlaceID).toBe(20); + }); + + it('should call getHRPDetails and alert on success', () => { + spyOn(component, 'getHRPDetails'); + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.updatePatientANC(component.patientANCDataForm); + expect(component.getHRPDetails).toHaveBeenCalled(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Updated', + 'success' + ); + }); + + it('should mark form as pristine on success', () => { + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + spyOn(component.patientANCDataForm, 'markAsPristine'); + component.updatePatientANC(component.patientANCDataForm); + expect(component.patientANCDataForm.markAsPristine).toHaveBeenCalled(); + }); + + it('should alert error when statusCode is not 200', () => { + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'Server Error' }) + ); + component.updatePatientANC(component.patientANCDataForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server Error', + 'error' + ); + }); + + it('should alert error when data is null', () => { + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: null, errorMessage: 'No data' }) + ); + component.updatePatientANC(component.patientANCDataForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No data', + 'error' + ); + }); + + it('should alert error on HTTP error', () => { + mockDoctorService.updateANCDetails.and.returnValue( + throwError('Network error') + ); + component.updatePatientANC(component.patientANCDataForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + + it('should normalize TT date fields when they have values', () => { + const immunizationForm = component.patientANCDataForm.get( + 'patientANCImmunizationForm' + ) as FormGroup; + immunizationForm.patchValue({ + dateReceivedForTT_1: new Date('2023-06-15'), + dateReceivedForTT_2: new Date('2023-07-15'), + dateReceivedForTT_3: null, + }); + + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'OK' } }) + ); + spyOn(component, 'getHRPDetails'); + component.updatePatientANC(component.patientANCDataForm); + + const tt1Value = immunizationForm.get('dateReceivedForTT_1')?.value; + const tt2Value = immunizationForm.get('dateReceivedForTT_2')?.value; + expect(typeof tt1Value).toBe('string'); + expect(typeof tt2Value).toBe('string'); + expect(tt1Value).toContain('T00:00:00.000Z'); + expect(tt2Value).toContain('T00:00:00.000Z'); + }); + + it('should normalize lmpDate and expDelDt when they have values', () => { + const ancDetailsForm = component.patientANCDataForm.get( + 'patientANCDetailsForm' + ) as FormGroup; + ancDetailsForm.patchValue({ + lmpDate: new Date('2023-01-10'), + expDelDt: new Date('2023-10-17'), + }); + + mockDoctorService.updateANCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'OK' } }) + ); + spyOn(component, 'getHRPDetails'); + component.updatePatientANC(component.patientANCDataForm); + + const lmpValue = ancDetailsForm.get('lmpDate')?.value; + const expValue = ancDetailsForm.get('expDelDt')?.value; + expect(typeof lmpValue).toBe('string'); + expect(typeof expValue).toBe('string'); + expect(lmpValue).toContain('T00:00:00.000Z'); + expect(expValue).toContain('T00:00:00.000Z'); + }); + }); + + describe('patchDataToFields', () => { + it('should call doctorService.getAncCareDetails with correct params', () => { + mockDoctorService.getAncCareDetails.and.returnValue(of({})); + component.patchDataToFields('200', '100'); + expect(mockDoctorService.getAncCareDetails).toHaveBeenCalledWith( + '200', + '100' + ); + }); + + it('should patch ANC details form on success', () => { + const mockResponse = { + statusCode: 200, + data: { + ANCCareDetail: { + primiGravida: true, + lmpDate: '2023-01-10T00:00:00Z', + expDelDt: '2023-10-17T00:00:00Z', + bloodGroup: 'A+', + }, + ANCWomenVaccineDetails: { + dateReceivedForTT_1: '2023-02-01T00:00:00Z', + dateReceivedForTT_2: '2023-03-01T00:00:00Z', + dateReceivedForTT_3: '2023-04-01T00:00:00Z', + }, + }, + }; + mockDoctorService.getAncCareDetails.and.returnValue(of(mockResponse)); + component.patchDataToFields('200', '100'); + + expect(component.gravidaStatus).toBeTrue(); + }); + + it('should disable bloodGroup when it is present and not "Don\'t Know"', () => { + const mockResponse = { + statusCode: 200, + data: { + ANCCareDetail: { + primiGravida: false, + lmpDate: '2023-01-10T00:00:00Z', + expDelDt: '2023-10-17T00:00:00Z', + bloodGroup: 'B+', + }, + ANCWomenVaccineDetails: null, + }, + }; + mockDoctorService.getAncCareDetails.and.returnValue(of(mockResponse)); + component.patchDataToFields('200', '100'); + + const bloodGroupControl = ( + component.patientANCDataForm.get('obstetricFormulaForm') as FormGroup + ).controls['bloodGroup']; + expect(bloodGroupControl.disabled).toBeTrue(); + }); + + it('should not disable bloodGroup when it is "Don\'t Know"', () => { + const mockResponse = { + statusCode: 200, + data: { + ANCCareDetail: { + primiGravida: false, + lmpDate: '2023-01-10T00:00:00Z', + expDelDt: '2023-10-17T00:00:00Z', + bloodGroup: "Don't Know", + }, + ANCWomenVaccineDetails: null, + }, + }; + mockDoctorService.getAncCareDetails.and.returnValue(of(mockResponse)); + component.patchDataToFields('200', '100'); + + const bloodGroupControl = ( + component.patientANCDataForm.get('obstetricFormulaForm') as FormGroup + ).controls['bloodGroup']; + expect(bloodGroupControl.disabled).toBeFalse(); + }); + + it('should alert error when statusCode is not 200', () => { + const mockResponse = { + statusCode: 500, + errorMessage: 'Failed to load', + data: null, + }; + mockDoctorService.getAncCareDetails.and.returnValue(of(mockResponse)); + component.patchDataToFields('200', '100'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Failed to load', + 'error' + ); + }); + + it('should alert error on HTTP failure', () => { + mockDoctorService.getAncCareDetails.and.returnValue( + throwError('Network error') + ); + component.patchDataToFields('200', '100'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + + it('should handle null ANCCareDetail gracefully', () => { + const mockResponse = { + statusCode: 200, + data: { + ANCCareDetail: null, + ANCWomenVaccineDetails: null, + }, + }; + mockDoctorService.getAncCareDetails.and.returnValue(of(mockResponse)); + expect(() => component.patchDataToFields('200', '100')).not.toThrow(); + }); + }); + + describe('getHRPDetails', () => { + it('should call doctorService.getHRPDetails with session data', () => { + mockDoctorService.getHRPDetails.and.returnValue(of({})); + component.getHRPDetails(); + expect(mockDoctorService.getHRPDetails).toHaveBeenCalledWith( + '200', + 'vc-1' + ); + }); + + it('should call setHRPPositive when isHRP is true', () => { + mockDoctorService.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + component.getHRPDetails(); + expect(mockBeneficiaryDetailsService.setHRPPositive).toHaveBeenCalled(); + }); + + it('should call resetHRPPositive when isHRP is false', () => { + mockDoctorService.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: false } }) + ); + component.getHRPDetails(); + expect(mockBeneficiaryDetailsService.resetHRPPositive).toHaveBeenCalled(); + }); + + it('should not call setHRPPositive or resetHRPPositive when statusCode is not 200', () => { + mockDoctorService.getHRPDetails.and.returnValue(of({ statusCode: 500 })); + component.getHRPDetails(); + expect( + mockBeneficiaryDetailsService.setHRPPositive + ).not.toHaveBeenCalled(); + expect( + mockBeneficiaryDetailsService.resetHRPPositive + ).not.toHaveBeenCalled(); + }); + + it('should not call setHRPPositive or resetHRPPositive when data is null', () => { + mockDoctorService.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.getHRPDetails(); + expect( + mockBeneficiaryDetailsService.setHRPPositive + ).not.toHaveBeenCalled(); + expect( + mockBeneficiaryDetailsService.resetHRPPositive + ).not.toHaveBeenCalled(); + }); + }); + + describe('normalizeToUTCMidnight (private)', () => { + it('should return null for null input', () => { + const result = (component as any).normalizeToUTCMidnight(null); + expect(result).toBeNull(); + }); + + it('should return null for undefined input', () => { + const result = (component as any).normalizeToUTCMidnight(undefined); + expect(result).toBeNull(); + }); + + it('should return a UTC midnight ISO string for a valid date', () => { + const result = (component as any).normalizeToUTCMidnight( + new Date('2023-06-15T14:30:00Z') + ); + expect(result).toBe('2023-06-15T00:00:00.000Z'); + }); + + it('should normalize different times on the same day to the same midnight', () => { + const result1 = (component as any).normalizeToUTCMidnight( + new Date(2023, 5, 15, 10, 30) + ); + const result2 = (component as any).normalizeToUTCMidnight( + new Date(2023, 5, 15, 22, 0) + ); + expect(result1).toBe(result2); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/anc/obstetric-formula/obstetric-formula.component.spec.ts b/src/app/app-modules/nurse-doctor/anc/obstetric-formula/obstetric-formula.component.spec.ts index 04669da8..03cc4b1f 100644 --- a/src/app/app-modules/nurse-doctor/anc/obstetric-formula/obstetric-formula.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/anc/obstetric-formula/obstetric-formula.component.spec.ts @@ -20,27 +20,161 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SimpleChange } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { MasterdataService } from '../../shared/services'; import { ObstetricFormulaComponent } from './obstetric-formula.component'; describe('ObstetricFormulaComponent', () => { let component: ObstetricFormulaComponent; let fixture: ComponentFixture; + let confirm: any; + let ben$: BehaviorSubject; + let master$: BehaviorSubject; + const info = LANGUAGE_EN.alerts.info; + + const makeForm = () => + new FormGroup({ + gravida_G: new FormControl(null), + termDeliveries_T: new FormControl(2), + pretermDeliveries_P: new FormControl(1), + livebirths_L: new FormControl(3), + abortions_A: new FormControl(null), + stillBirth: new FormControl(null), + bloodGroup: new FormControl(null), + }); + const form = () => component.obstetricFormulaForm; + const change = { gravidaStatus: new SimpleChange(null, true, true) }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + ben$ = new BehaviorSubject(null); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ObstetricFormulaComponent], + providers: [ + ...commonTestProviders(), + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ObstetricFormulaComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + component.obstetricFormulaForm = makeForm(); + component.ngOnInit(); + component.ngDoCheck(); + }); + + afterEach(() => component.beneficiaryDetailsSubscription?.unsubscribe()); + + it('loads blood groups then patches beneficiary blood group', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.beneficiaryDetailsSubscription).toBeUndefined(); + master$.next({ bloodGroups: ['A+', 'B+'] }); + expect(component.selectBloodGroupType).toEqual(['A+', 'B+']); + ben$.next({ bloodGroup: 'A+' }); + expect(form().value.bloodGroup).toBe('A+'); + expect(component.bloodGroup).toBe('A+'); + expect(component.disableBloodGroup).toBeTrue(); + }); + + it("does not disable blood group for Don't Know, or patch when absent", () => { + master$.next({ bloodGroups: [] }); + ben$.next({ bloodGroup: "Don't Know" }); + expect(form().value.bloodGroup).toBe("Don't Know"); + expect(component.disableBloodGroup).toBeFalse(); + ben$.next({}); + expect(form().value.bloodGroup).toBe("Don't Know"); + }); + + it('does not patch blood group in view mode', () => { + component.mode = 'view'; + master$.next({ bloodGroups: [] }); + ben$.next({ bloodGroup: 'O+' }); + expect(form().value.bloodGroup).toBeNull(); + }); + + it('ngOnChanges resets counts for primigravida', () => { + component.gravidaStatus = true; + component.ngOnChanges(change); + expect(form().value).toEqual( + jasmine.objectContaining({ + gravida_G: 1, + termDeliveries_T: null, + pretermDeliveries_P: null, + livebirths_L: null, + abortions_A: null, + stillBirth: null, + }) + ); + }); + + it('ngOnChanges calculates gravida when not primigravida, ignores other changes', () => { + component.gravidaStatus = false; + component.ngOnChanges(change); + expect(component.gravida_G).toBe(4); + form().patchValue({ gravida_G: 99 }); + component.ngOnChanges({}); + expect(component.gravida_G).toBe(99); + }); + + it('calculateGravida sums all pregnancies and alerts on outliers', () => { + form().patchValue({ abortions_A: 6, stillBirth: 10 }); + component.calculateGravida('abortions_A'); + expect(component.gravida_G).toBe(20); + component.calculateGravida('stillBirth'); + component.calculateGravida('termDeliveries_T'); + expect(confirm.alert.calls.allArgs()).toEqual([ + [info.recheckValue], + [info.recheckValue], + ]); + form().patchValue({ abortions_A: 1, stillBirth: 1 }); + component.calculateGravida('abortions_A'); + component.calculateGravida('stillBirth'); + expect(confirm.alert).toHaveBeenCalledTimes(2); + expect(component.gravida_G).toBe(6); + expect(component.abortions_A).toBe(1); + expect(component.stillBirth).toBe(1); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('range checks alert only for large values', () => { + component.checkLivingChildren(9); + component.checkAbortions(5); + component.checkStillBirth(9); + expect(confirm.alert).not.toHaveBeenCalled(); + component.checkLivingChildren(10); + component.checkAbortions(6); + component.checkStillBirth(10); + expect(confirm.alert.calls.allArgs()).toEqual([ + [info.recheckValue], + [info.valueRange], + ['value can not be greater than 9'], + ]); + expect(component.livebirths_L).toBe(3); + expect(component.termDeliveries_T).toBe(2); + expect(component.pretermDeliveries_P).toBe(1); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/beneficiary-mcts-call-history/beneficiary-mcts-call-history.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/beneficiary-mcts-call-history/beneficiary-mcts-call-history.component.spec.ts index a502949c..d74a0450 100644 --- a/src/app/app-modules/nurse-doctor/case-record/beneficiary-mcts-call-history/beneficiary-mcts-call-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/beneficiary-mcts-call-history/beneficiary-mcts-call-history.component.spec.ts @@ -19,28 +19,70 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BeneficiaryMctsCallHistoryComponent } from './beneficiary-mcts-call-history.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('BeneficiaryMctsCallHistoryComponent', () => { let component: BeneficiaryMctsCallHistoryComponent; let fixture: ComponentFixture; + const calls = Array.from({ length: 7 }, (_, i) => ({ + questionnaireDetail: { question: i % 2 ? `Fever ${i}` : `Cough ${i}` }, + answer: 'yes', + })); - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [BeneficiaryMctsCallHistoryComponent], - }).compileComponents(); - })); + providers: [...commonTestProviders({ dialogData: calls })], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(BeneficiaryMctsCallHistoryComponent, '') + .compileComponents(); - beforeEach(() => { fixture = TestBed.createComponent(BeneficiaryMctsCallHistoryComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('should create and load call details with first page', () => { expect(component).toBeTruthy(); + expect(component.callDetails).toBe(calls); + expect(component.filteredCallDetails).toBe(calls); + expect(component.callDetailsPagedList.length).toBe(5); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('pages call details', () => { + component.callDetailsPageChanged({ page: 2, itemsPerPage: 5 }); + expect(component.callDetailsPagedList).toEqual(calls.slice(5, 10)); + }); + + it('filters by question (case insensitive) and resets page', () => { + component.callDetailsActivePage = 2; + component.filterCallHistory('FEVER'); + expect(component.filteredCallDetails.length).toBe(3); + expect(component.callDetailsActivePage).toBe(1); + expect(component.callDetailsPagedList.length).toBe(3); + }); + + it('restores full list on empty search term', () => { + component.filterCallHistory('fever'); + component.filterCallHistory(''); + expect(component.filteredCallDetails).toBe(calls); + expect(component.callDetailsPagedList.length).toBe(5); + }); + + it('refreshes language on ngDoCheck', () => { + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/beneficiary-platform-history/beneficiary-platform-history.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/beneficiary-platform-history/beneficiary-platform-history.component.spec.ts index 3f2d275e..79200e85 100644 --- a/src/app/app-modules/nurse-doctor/case-record/beneficiary-platform-history/beneficiary-platform-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/beneficiary-platform-history/beneficiary-platform-history.component.spec.ts @@ -20,27 +20,374 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DoctorService } from '../../shared/services'; +import { CaseSheetComponent } from '../../case-sheet/case-sheet.component'; +import { BeneficiaryMctsCallHistoryComponent } from '../beneficiary-mcts-call-history/beneficiary-mcts-call-history.component'; import { BeneficiaryPlatformHistoryComponent } from './beneficiary-platform-history.component'; describe('BeneficiaryPlatformHistoryComponent', () => { let component: BeneficiaryPlatformHistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let confirm: any; + let dialog: any; + let session: any; + let router: Router; + + const services = () => [ + { serviceID: 1, serviceName: '1097' }, + { serviceID: 2, serviceName: 'MMU' }, + { serviceID: 3, serviceName: '104' }, + { serviceID: 4, serviceName: 'TM' }, + { serviceID: 5, serviceName: 'ECD' }, + { serviceID: 6, serviceName: 'MCTS' }, + ]; + + const loaded = (id: number) => + component.serviceOnState.find((s: any) => s.serviceID === id).serviceLoaded; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + doctor = autoSpy(DoctorService); + doctor.getServiceOnState.and.returnValue( + of({ statusCode: 200, data: services() }) + ); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [BeneficiaryPlatformHistoryComponent], + providers: [ + ...commonTestProviders(), + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(BeneficiaryPlatformHistoryComponent); component = fixture.componentInstance; + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('loads language and services except 1097/ECD', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.serviceOnState).toEqual([ + { serviceID: 2, serviceName: 'MMU', serviceLoaded: false }, + { serviceID: 3, serviceName: '104', serviceLoaded: false }, + { serviceID: 4, serviceName: 'TM', serviceLoaded: false }, + { serviceID: 6, serviceName: 'MCTS', serviceLoaded: false }, + ]); + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(buttons.length).toBeGreaterThanOrEqual(4); + }); + + it('keeps services empty on non-200 service response', () => { + doctor.getServiceOnState.and.returnValue(of({ statusCode: 500 })); + component.serviceOnState = []; + component.getServiceOnState(); + expect(component.serviceOnState).toEqual([]); + }); + + it('getServiceHistory routes to the right loader', () => { + spyOn(component, 'getMMUHistory'); + spyOn(component, 'get104History'); + spyOn(component, 'getTMHistory'); + spyOn(component, 'getMCTSHistory'); + component.getServiceHistory(2); + component.getServiceHistory(3); + component.getServiceHistory(4); + component.getServiceHistory(6); + component.getServiceHistory(9); + expect(component.getMMUHistory).toHaveBeenCalledTimes(1); + expect(component.get104History).toHaveBeenCalledTimes(1); + expect(component.getTMHistory).toHaveBeenCalledTimes(1); + expect(component.getMCTSHistory).toHaveBeenCalledTimes(1); + }); + + describe('MMU history', () => { + const visits = () => [ + { + visitCode: 11, + VisitCategory: 'General OPD', + benFlowID: 1, + beneficiaryRegID: 2, + }, + { visitCode: null, VisitCategory: 'ANC' }, + ]; + + it('loads history and fetches casesheet per visit with code', () => { + doctor.getMMUHistory.and.returnValue( + of({ statusCode: 200, data: visits() }) + ); + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: { nurseData: {} } }) + ); + component.getMMUHistory(); + expect(component.hideMMUFetch).toBeTrue(); + expect(loaded(2)).toBeTrue(); + expect(loaded(3)).toBeFalse(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledOnceWith({ + VisitCategory: 'General OPD', + benFlowID: 1, + beneficiaryRegID: 2, + visitCode: 11, + }); + expect(component.dataSource.data[0].benPreviousData).toEqual({ + nurseData: {}, + }); + expect(component.filteredMMUHistory).toEqual({ nurseData: {} }); + expect(component.previousMMUHistoryPagedList.length).toBe(2); + }); + + it('ignores casesheet responses with null data', () => { + doctor.getMMUHistory.and.returnValue( + of({ statusCode: 200, data: visits() }) + ); + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.getMMUHistory(); + expect(component.dataSource.data[0].benPreviousData).toBeUndefined(); + }); + + it('does nothing on non-200 history', () => { + doctor.getMMUHistory.and.returnValue(of({ statusCode: 500 })); + component.getMMUHistory(); + expect(component.hideMMUFetch).toBeFalse(); + expect(loaded(2)).toBeFalse(); + }); + + it('filterMMUHistory without term resets to all data', () => { + component.dataSource.data = visits(); + component.previousMMUHistoryActivePage = 3; + component.filterMMUHistory(''); + expect(component.filteredMMUHistory.length).toBe(2); + expect(component.previousMMUHistoryActivePage).toBe(1); + expect(component.previousMMUHistoryPagedList.length).toBe(2); + }); + + it('filterMMUHistory with term filters by category', () => { + component.dataSource.data = visits(); + component.filterMMUHistory('anc'); + expect(component.filteredMMUHistory.length).toBe(1); + expect(component.filteredMMUHistory[0].VisitCategory).toBe('ANC'); + }); + + it('previousMMUHistoryPageChanged slices current page', () => { + component.dataSource.data = [1, 2, 3, 4, 5, 6, 7]; + component.previousMMUHistoryPageChanged({ page: 2, itemsPerPage: 5 }); + expect(component.previousMMUHistoryPagedList).toEqual([6, 7]); + }); + }); + + describe('TM history', () => { + it('loads TM history and marks service loaded', () => { + doctor.getTMHistory.and.returnValue( + of({ + statusCode: 200, + data: [{ VisitCategory: 'NCD' }, { VisitCategory: 'PNC' }], + }) + ); + component.getTMHistory(); + expect(component.hideTMFetch).toBeTrue(); + expect(loaded(4)).toBeTrue(); + expect(component.filteredTMHistory.length).toBe(2); + expect(component.previousTMHistoryPagedList.length).toBe(2); + }); + + it('ignores non-200 TM response', () => { + doctor.getTMHistory.and.returnValue(of({ statusCode: 500 })); + component.getTMHistory(); + expect(component.hideTMFetch).toBeFalse(); + }); + + it('filterTMHistory filters and resets', () => { + component.historyOfTM.data = [ + { VisitCategory: 'NCD' }, + { VisitCategory: 'PNC' }, + ]; + component.filterTMHistory('pn'); + expect(component.filteredTMHistory).toEqual([{ VisitCategory: 'PNC' }]); + expect(component.previousTMHistoryPagedList).toEqual([ + { VisitCategory: 'PNC' }, + ] as any); + component.filterTMHistory(); + expect(component.filteredTMHistory).toBe(component.historyOfTM.data); + expect(component.previousTMHistoryActivePage).toBe(1); + }); + }); + + describe('MCTS history', () => { + const calls = () => [ + { mctsOutboundCall: { displayOBCallType: 'ANC Call' } }, + { mctsOutboundCall: { displayOBCallType: 'PNC Call' } }, + ]; + + it('loads MCTS history', () => { + doctor.getMCTSHistory.and.returnValue( + of({ statusCode: 200, data: calls() }) + ); + component.getMCTSHistory(); + expect(component.hideMCTSFetch).toBeTrue(); + expect(loaded(6)).toBeTrue(); + expect(component.previousMCTSHistoryPagedList.length).toBe(2); + }); + + it('ignores non-200 MCTS response', () => { + doctor.getMCTSHistory.and.returnValue(of({ statusCode: 500 })); + component.getMCTSHistory(); + expect(component.hideMCTSFetch).toBeFalse(); + }); + + it('filterMCTSHistory filters by call type and resets', () => { + component.historyOfMCTS.data = calls(); + component.filterMCTSHistory('anc'); + expect(component.filteredMCTSHistory.length).toBe(1); + expect( + component.filteredMCTSHistory[0].mctsOutboundCall.displayOBCallType + ).toBe('ANC Call'); + component.filterMCTSHistory(''); + expect(component.filteredMCTSHistory).toBe(component.historyOfMCTS.data); + }); + + it('getPatientMCTSCallHistory opens call dialog on success', () => { + doctor.getPatientMCTSCallHistory.and.returnValue( + of({ statusCode: 200, data: [{ id: 1 }] }) + ); + component.getPatientMCTSCallHistory({ callDetailID: 44 }); + expect(doctor.getPatientMCTSCallHistory).toHaveBeenCalledWith({ + callDetailID: 44, + }); + expect(dialog.open).toHaveBeenCalledWith( + BeneficiaryMctsCallHistoryComponent, + { + width: '70%', + panelClass: 'preview-casesheet', + data: [{ id: 1 }], + } + ); + }); + + it('getPatientMCTSCallHistory does not open dialog on failure', () => { + doctor.getPatientMCTSCallHistory.and.returnValue(of({ statusCode: 500 })); + component.getPatientMCTSCallHistory({ callDetailID: 44 }); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + it('showCallDetails tolerates a truthy dialog result', () => { + dialog.open.and.returnValue({ afterClosed: () => of(true) }); + expect(() => component.showCallDetails({})).not.toThrow(); + }); + }); + + describe('104 history', () => { + it('loads 104 history', () => { + doctor.get104History.and.returnValue( + of({ statusCode: 200, data: [{ diseaseSummary: 'Fever' }] }) + ); + component.get104History(); + expect(component.hide104Fetch).toBeTrue(); + expect(loaded(3)).toBeTrue(); + expect(component.previous104HistoryPagedList.length).toBe(1); + }); + + it('ignores non-200 104 response', () => { + doctor.get104History.and.returnValue(of({ statusCode: 500 })); + component.get104History(); + expect(component.hide104Fetch).toBeFalse(); + }); + + it('filter104History filters by disease summary and resets', () => { + component.historyOf104.data = [ + { diseaseSummary: 'Fever' }, + { diseaseSummary: 'Cough' }, + ]; + component.filter104History('COU'); + expect(component.filtered104History).toEqual([ + { diseaseSummary: 'Cough' }, + ]); + component.filter104History(); + expect(component.filtered104History).toBe(component.historyOf104.data); + }); + }); + + describe('getVisitDetails', () => { + const visit = { + visitCode: 1, + benFlowID: 2, + VisitCategory: 'ANC', + beneficiaryRegID: 3, + benVisitID: 4, + }; + + it('stores visit and opens preview dialog', () => { + component.getVisitDetails('MMU', visit, false); + expect(confirm.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.viewCasesheet + ); + expect(session.setItem).toHaveBeenCalledWith( + 'previousCaseSheetVisitCode', + 1 + ); + expect(session.setItem).toHaveBeenCalledWith( + 'previousCaseSheetBenFlowID', + 2 + ); + expect(session.setItem).toHaveBeenCalledWith( + 'previousCaseSheetVisitCategory', + 'ANC' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'previousCaseSheetBeneficiaryRegID', + 3 + ); + expect(session.setItem).toHaveBeenCalledWith( + 'previousCaseSheetVisitID', + 4 + ); + expect(dialog.open).toHaveBeenCalledWith(CaseSheetComponent, { + disableClose: true, + width: '95%', + panelClass: 'preview-casesheet', + data: { previous: true, serviceType: 'MMU' }, + }); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('navigates to print page when printing', () => { + component.getVisitDetails('MMU', visit, true); + expect(confirm.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.printCasesheet + ); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/previous', + ]); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + it('does nothing when not confirmed', () => { + confirm.confirm.and.returnValue(of(false)); + component.getVisitDetails('TM', visit, true); + expect(session.setItem).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/cancer-case-record/cancer-case-record.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/cancer-case-record/cancer-case-record.component.spec.ts index ae6c72d1..54ccaefe 100644 --- a/src/app/app-modules/nurse-doctor/case-record/cancer-case-record/cancer-case-record.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/cancer-case-record/cancer-case-record.component.spec.ts @@ -20,27 +20,374 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { CameraService } from 'src/app/app-modules/core/services'; +import { DoctorService } from '../../shared/services'; import { CancerCaseRecordComponent } from './cancer-case-record.component'; describe('CancerCaseRecordComponent', () => { let component: CancerCaseRecordComponent; let fixture: ComponentFixture; + let doctor: any; + let camera: any; + let ben$: BehaviorSubject; + let session: any; + let router: Router; + + const vitals = { weight_Kg: 64, height_cm: 160 }; + const diagnosisForm = () => + new FormGroup({ + provisionalDiagnosisPrimaryDoctor: new FormControl(null), + remarks: new FormControl(null), + provisionalDiagnosisOncologist: new FormControl(null), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async ( + opts: { + ben?: any; + mode?: string; + sessionData?: Record; + } = {} + ) => { + ben$ = new BehaviorSubject(opts.ben ?? null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerCaseRecordComponent], + providers: [ + ...commonTestProviders({ session: opts.sessionData }), + { provide: DoctorService, useValue: doctor }, + { provide: CameraService, useValue: camera }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerCaseRecordComponent); component = fixture.componentInstance; + component.diagnosisForm = diagnosisForm(); + component.caseRecordMode = opts.mode ?? 'new'; + component.currentVitals = vitals; + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture.detectChanges(); + }; + + beforeEach(() => { + doctor = autoSpy(DoctorService); + camera = autoSpy(CameraService); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture?.destroy()); + + describe('with no beneficiary', () => { + beforeEach(async () => setup()); + + it('sets language and fetches nothing', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.female).toBeUndefined(); + expect(doctor.getCancerVitalsDetails).not.toHaveBeenCalled(); + }); + + it('calculateBMI computes BMI or 0 without vitals', () => { + expect(component.calculateBMI()).toBe(25); + component.currentVitals = null; + expect(component.calculateBMI()).toBe(0); + }); + + it('checkNormalWaist uses 90cm cutoff for non-female', () => { + component.checkNormalWaist(85); + expect(component.normalWaist).toBeTrue(); + component.checkNormalWaist(95); + expect(component.normalWaist).toBeFalse(); + }); + + it('checkNormalWaist uses 80cm cutoff for non-pregnant female', () => { + component.female = true; + component.pregnancyStatus = 'No'; + component.checkNormalWaist(85); + expect(component.normalWaist).toBeFalse(); + component.checkNormalWaist(75); + expect(component.normalWaist).toBeTrue(); + component.pregnancyStatus = 'Yes'; + component.checkNormalWaist(85); + expect(component.normalWaist).toBeTrue(); + }); + + it('patchDiagnosisDetails patches the form', () => { + component.patchDiagnosisDetails({ remarks: 'r1' }); + expect(component.diagnosisForm.value.remarks).toBe('r1'); + }); + + it('getDiagnosisDetails patches when diagnosis present', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { diagnosis: { provisionalDiagnosisOncologist: 'CA' } }, + }) + ); + component.getDiagnosisDetails(1, 2, 'Cancer Screening'); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 1, + 2, + 'Cancer Screening' + ); + expect(component.diagnosisForm.value.provisionalDiagnosisOncologist).toBe( + 'CA' + ); + }); + + it('getDiagnosisDetails ignores response without diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getDiagnosisDetails(1, 2, 'x'); + expect(component.diagnosisForm.value.remarks).toBeNull(); + }); + + it('getCaseSheetPrintData stores details and navigates', () => { + component.getCaseSheetPrintData({ + createdDate: '2024-01-02T00:00:00.000Z', + VisitCategory: 'Cancer Screening', + beneficiaryRegID: 5, + benVisitID: 6, + }); + expect(component.visitDateTime).toBe('2024-01-02T00:00:00.000Z'); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetBenFlowID', + 'null' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategory', + 'Cancer Screening' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetBeneficiaryRegID', + 5 + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetVisitID', 6); + expect(router.navigate).toHaveBeenCalledWith(['/nurse-doctor/print']); + }); + + it('renders findings section only when findings present', () => { + expect(fixture.nativeElement.textContent).not.toContain('history-text'); + component.findings = { briefHistory: 'history-text' }; + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('history-text'); + }); + + it('ngOnDestroy unsubscribes subscriptions', () => { + component.getDiagnosisDetails(1, 2, 'x'); + const s1 = component.beneficiaryDetailsSubscription; + const s2 = component.diagnosisSubscription; + spyOn(s1, 'unsubscribe').and.callThrough(); + spyOn(s2, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(s1.unsubscribe).toHaveBeenCalled(); + expect(s2.unsubscribe).toHaveBeenCalled(); + }); + + it('ngOnDestroy tolerates missing subscriptions', () => { + component.beneficiaryDetailsSubscription.unsubscribe(); + component.beneficiaryDetailsSubscription = null; + component.diagnosisSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('graphs', () => { + beforeEach(async () => setup()); + + it('plots BP, weight and glucose graphs newest first', () => { + component.movegraphData({ + bpList: [ + { date: '2024-01-01', avgSysBP: 120, avgDysBP: 80 }, + { date: '2024-02-01', avgSysBP: 130, avgDysBP: 85 }, + { date: '2024-03-01', avgSysBP: null, avgDysBP: 70 }, + ], + weightList: [ + { date: '2024-01-01', weight: 60 }, + { date: '2024-02-01', weight: 62 }, + { date: '2024-03-01', weight: null }, + ], + bgList: [ + { + date: '2024-01-01', + bg_2hr_pp: 140, + bg_fasting: 90, + bg_random: 110, + }, + { date: '2024-02-01', bg_2hr_pp: 150 }, + ], + }); + expect(component.bpChartLabels).toEqual(['2024-02-01', '2024-01-01']); + expect(component.bpChartData).toEqual([ + { data: [130, 120], label: 'Systolic BP' }, + { data: [85, 80], label: 'Diastolic BP' }, + ]); + expect(component.weightChartData).toEqual([ + { data: [62, 60], label: 'Weight' }, + ]); + expect(component.weightChartLabels).toEqual(['2024-02-01', '2024-01-01']); + expect(component.bgChartLabels).toEqual(['2024-01-01']); + expect(component.bgChartData).toEqual([ + { data: [140], label: '2-Hr Post Prandial' }, + { data: [90], label: 'Fasting' }, + { data: [110], label: 'Random' }, + ]); + }); + + it('adds no series for empty lists', () => { + component.movegraphData({ bpList: [], weightList: [], bgList: [] }); + expect(component.bpChartData).toEqual([]); + expect(component.weightChartData).toEqual([]); + expect(component.bgChartData).toEqual([]); + }); + + it('chartClicked opens the matching graph', () => { + component.weightChartData = [{ data: [1] }]; + component.chartClicked('bw'); + expect(camera.ViewGraph).toHaveBeenCalledWith( + jasmine.objectContaining({ + type: 'bw', + chartData: [{ data: [1] }], + chartType: 'line', + chartColors: component.weightChartColors, + }) + ); + component.chartClicked('bp'); + expect(camera.ViewGraph.calls.mostRecent().args[0].type).toBe('bp'); + expect(camera.ViewGraph.calls.mostRecent().args[0].chartColors).toBe( + component.bpChartColors + ); + component.chartClicked('bg'); + expect(camera.ViewGraph.calls.mostRecent().args[0].type).toBe('bg'); + expect(camera.ViewGraph).toHaveBeenCalledTimes(3); + component.chartClicked('unknown'); + expect(camera.ViewGraph).toHaveBeenCalledTimes(3); + }); + + it('graph callers skip incomplete graph objects', () => { + component.callBodyWeightGraph({ type: 'bw' }); + component.callBloodPressureGraph({}); + component.callBloodGlucoseGraph({ a: 1 }); + expect(camera.ViewGraph).not.toHaveBeenCalled(); + }); + }); + + describe('MMU history', () => { + beforeEach(async () => setup()); + + it('loads history on 200', () => { + doctor.getMMUHistory.and.returnValue( + of({ + statusCode: 200, + data: [{ VisitCategory: 'Cancer' }, { VisitCategory: 'ANC' }], + }) + ); + component.getMMUHistory(); + expect(component.hideMMUFetch).toBeTrue(); + expect(component.previousMMUHistoryPagedList.length).toBe(2); + }); + + it('ignores non-200 history', () => { + doctor.getMMUHistory.and.returnValue(of({ statusCode: 500 })); + component.getMMUHistory(); + expect(component.hideMMUFetch).toBeFalse(); + }); + + it('filterMMUHistory filters by category and resets', () => { + component.historyOfMMU = [ + { VisitCategory: 'Cancer' }, + { VisitCategory: 'ANC' }, + ] as any; + component.previousMMUHistoryActivePage = 2; + component.filterMMUHistory('canc'); + expect(component.filteredMMUHistory).toEqual([ + { VisitCategory: 'Cancer' }, + ]); + expect(component.previousMMUHistoryActivePage).toBe(1); + expect(component.previousMMUHistoryPagedList).toEqual([ + { VisitCategory: 'Cancer' }, + ] as any); + component.filterMMUHistory(); + expect(component.filteredMMUHistory.length).toBe(2); + }); + }); + + describe('with female beneficiary', () => { + const ben = { genderName: 'Female', beneficiaryRegID: 3, benVisitID: 4 }; + + it('fetches graph data and plots it', async () => { + doctor.getCancerVitalsDetails.and.returnValue( + of({ + statusCode: 200, + data: { + GraphData: { + bpList: [], + weightList: [{ date: '2024-01-01', weight: 50 }], + bgList: [], + }, + }, + }) + ); + await setup({ ben }); + expect(component.female).toBeTrue(); + expect(doctor.getCancerVitalsDetails).toHaveBeenCalledWith(3, 4); + expect(component.weightChartData).toEqual([ + { data: [50], label: 'Weight' }, + ]); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('skips plotting when graph response is not 200', async () => { + doctor.getCancerVitalsDetails.and.returnValue(of({ statusCode: 500 })); + await setup({ ben: { genderName: 'Male', beneficiaryRegID: 3 } }); + expect(component.female).toBeUndefined(); + expect(component.weightChartData).toEqual([]); + }); + + it('fetches diagnosis in view mode when doctorFlag is 9', async () => { + doctor.getCancerVitalsDetails.and.returnValue(of({ statusCode: 500 })); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: { diagnosis: { remarks: 'seen' } } }) + ); + await setup({ + ben, + mode: 'view', + sessionData: { + beneficiaryRegID: 3, + visitID: 4, + visitCategory: 'Cancer Screening', + doctorFlag: '9', + }, + }); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 3, + 4, + 'Cancer Screening' + ); + expect(component.diagnosisForm.value.remarks).toBe('seen'); + }); + + it('does not fetch diagnosis in view mode for other doctor flags', async () => { + doctor.getCancerVitalsDetails.and.returnValue(of({ statusCode: 500 })); + await setup({ ben, mode: 'view', sessionData: { doctorFlag: '1' } }); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/case-record.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/case-record.component.spec.ts index fdaedb11..441ad741 100644 --- a/src/app/app-modules/nurse-doctor/case-record/case-record.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/case-record.component.spec.ts @@ -20,27 +20,73 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from 'src/testing/test-utils'; import { CaseRecordComponent } from './case-record.component'; describe('CaseRecordComponent', () => { let component: CaseRecordComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [CaseRecordComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CaseRecordComponent); component = fixture.componentInstance; + component.patientCaseRecordForm = new FormGroup({}); + }); + + afterEach(() => fixture.destroy()); + + const render = (cat: any) => { + component.visitCategory = cat; fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + return { + general: el.querySelector('app-general-case-record'), + cancer: el.querySelector('app-cancer-case-record'), + }; + }; + + [ + 'General OPD', + 'ANC', + 'NCD care', + 'PNC', + 'COVID-19 Screening', + 'NCD screening', + ].forEach(cat => { + it(`shows general case record for "${cat}"`, () => { + const r = render(cat); + expect(component.showGeneralOPD).toBeTrue(); + expect(component.showCancer).toBeFalse(); + expect(r.general).not.toBeNull(); + expect(r.cancer).toBeNull(); + }); + }); + + it('shows cancer case record for Cancer Screening', () => { + const r = render('Cancer Screening'); + expect(component.showCancer).toBeTrue(); + expect(component.showGeneralOPD).toBeFalse(); + expect(r.cancer).not.toBeNull(); + expect(r.general).toBeNull(); + }); + + it('shows nothing for other categories', () => { + const r = render('FP & Contraceptive Services'); + expect(component.showCancer).toBeFalse(); + expect(component.showGeneralOPD).toBeFalse(); + expect(r.general).toBeNull(); + expect(r.cancer).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('shows nothing when visit category is missing', () => { + const r = render(undefined); + expect(component.showGeneralOPD).toBeFalse(); + expect(r.general).toBeNull(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/anc-diagnosis/anc-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/anc-diagnosis/anc-diagnosis.component.spec.ts index 198a6791..2a289415 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/anc-diagnosis/anc-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/anc-diagnosis/anc-diagnosis.component.spec.ts @@ -20,27 +20,304 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services'; +import { DoctorService, MasterdataService } from '../../../../shared/services'; import { AncDiagnosisComponent } from './anc-diagnosis.component'; describe('AncDiagnosisComponent', () => { let component: AncDiagnosisComponent; let fixture: ComponentFixture; + let doctor: any; + let benSvc: any; + let nurseMaster$: BehaviorSubject; + let hrp$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [AncDiagnosisComponent], - }).compileComponents(); - })); + const masterData = { + pregComplicationTypes: [ + { pregComplicationID: 1, pregComplicationType: 'None' }, + { pregComplicationID: 2, pregComplicationType: 'Other' }, + { pregComplicationID: 3, pregComplicationType: 'Hypothyroidism' }, + { pregComplicationID: 4, pregComplicationType: 'Anemia' }, + ], + }; + + const buildForm = () => + new FormGroup({ + gravida_G: new FormControl(null), + duration: new FormControl(null), + highRiskStatus: new FormControl(null), + highRiskCondition: new FormControl(null), + complicationOfCurrentPregnancyList: new FormControl([]), + otherCurrPregComplication: new FormControl('keep'), + isMaternalDeath: new FormControl(null), + placeOfDeath: new FormControl('Home'), + dateOfDeath: new FormControl('2024-01-01'), + causeOfDeath: new FormControl('Bleeding'), + }); - beforeEach(() => { + const create = (mode = 'new', master: any = masterData) => { + nurseMaster$.next(master); fixture = TestBed.createComponent(AncDiagnosisComponent); component = fixture.componentInstance; + component.generalDiagnosisForm = buildForm(); + component.caseRecordMode = mode; fixture.detectChanges(); + }; + + beforeEach(async () => { + nurseMaster$ = new BehaviorSubject(null); + hrp$ = new BehaviorSubject(0); + doctor = autoSpy(DoctorService); + benSvc = autoSpy(BeneficiaryDetailsService, { + HRPPositiveFlag$: hrp$.asObservable(), + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [AncDiagnosisComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: 'B1', + visitCode: 'VC1', + visitID: 'V1', + visitCategory: 'ANC', + }, + }), + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: nurseMaster$.asObservable() }, + }, + { provide: BeneficiaryDetailsService, useValue: benSvc }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(AncDiagnosisComponent, '') + .compileComponents(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture?.destroy()); + + describe('ngOnInit', () => { + it('resets HRP, fetches HRP details and master data', () => { + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + create(); + expect(benSvc.resetHRPPositive).toHaveBeenCalled(); + expect(doctor.getHRPDetails).toHaveBeenCalledWith('B1', 'VC1'); + expect(component.masterData).toBe(masterData); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + const diff = + component.today.getTime() - component.minimumDeathDate.getTime(); + expect(diff).toBe(365 * 24 * 60 * 60 * 1000); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('HRP flag stream overrides showHRP', () => { + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + create(); + // flag stream (0) subscribed after fetch -> false + expect(component.showHRP).toBe('false'); + hrp$.next(2); + expect(component.showHRP).toBe('true'); + hrp$.next(0); + expect(component.showHRP).toBe('false'); + }); + + it('fetchHPRPositive maps isHRP to showHRP', () => { + create(); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + component.fetchHPRPositive(); + expect(component.showHRP).toBe('true'); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: false } }) + ); + component.fetchHPRPositive(); + expect(component.showHRP).toBe('false'); + component.showHRP = 'unchanged'; + doctor.getHRPDetails.and.returnValue(of({ statusCode: 500 })); + component.fetchHPRPositive(); + expect(component.showHRP).toBe('unchanged'); + }); + + it('keeps masterData undefined when master stream is null', () => { + create('new', null); + expect(component.masterData).toBeUndefined(); + }); + + it('fetches and patches diagnosis in view mode', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + highRiskStatus: 'Yes', + dateOfDeath: '2024-02-03T00:00:00.000Z', + complicationOfCurrentPregnancyList: [ + { pregComplicationType: 'Other' }, + { pregComplicationType: 'Unknown' }, + ], + otherCurrPregComplication: 'something', + }, + }, + }) + ); + create('view'); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'ANC' + ); + const v = component.generalDiagnosisForm.value; + expect(v.highRiskStatus).toBe('Yes'); + expect(v.dateOfDeath instanceof Date).toBeTrue(); + expect(v.complicationOfCurrentPregnancyList).toEqual([ + masterData.pregComplicationTypes[1], + ]); + expect(v.otherCurrPregComplication).toBe('something'); + expect(component.showOtherPregnancyComplication).toBeTrue(); + }); + + it('does not patch when view response lacks diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + create('view'); + expect(component.generalDiagnosisForm.value.highRiskStatus).toBeNull(); + }); + }); + + describe('after init', () => { + beforeEach(() => create()); + + it('patchComplicationOfCurrentPregnancyList drops all without master data', () => { + component.masterData = null; + const diagnosis: any = { + complicationOfCurrentPregnancyList: [{ pregComplicationType: 'None' }], + }; + component.patchComplicationOfCurrentPregnancyList(diagnosis); + expect(diagnosis.complicationOfCurrentPregnancyList).toEqual([]); + expect(component.showAllPregComplication).toBeTrue(); + }); + + it('patchDiagnosisDetails keeps null death date', () => { + component.patchDiagnosisDetails({ + dateOfDeath: null, + complicationOfCurrentPregnancyList: [], + }); + expect(component.generalDiagnosisForm.value.dateOfDeath).toBeNull(); + }); + + it('exposes form getters', () => { + component.generalDiagnosisForm.patchValue({ + highRiskStatus: 'No', + highRiskCondition: 'c', + complicationOfCurrentPregnancyList: [{ pregComplicationType: 'x' }], + }); + expect(component.highRiskStatus?.value).toBe('No'); + expect(component.highRiskCondition?.value).toBe('c'); + expect(component.complicationOfCurrentPregnancyList).toEqual([ + { pregComplicationType: 'x' }, + ]); + }); + + it('checkWithDeathDetails clears death fields', () => { + component.checkWithDeathDetails(); + const v = component.generalDiagnosisForm.value; + expect(v.placeOfDeath).toBeNull(); + expect(v.dateOfDeath).toBeNull(); + expect(v.causeOfDeath).toBeNull(); + }); + + describe('resetOtherPregnancyComplication', () => { + it('multiple complications disable None and hide all', () => { + component.resetOtherPregnancyComplication( + [ + { pregComplicationType: 'Anemia' }, + { pregComplicationType: 'Other' }, + ], + 0 + ); + expect(component.showOtherPregnancyComplication).toBeTrue(); + expect(component.disableNonePregnancyComplication).toBeTrue(); + expect(component.showAllPregComplication).toBeFalse(); + expect( + component.generalDiagnosisForm.value.otherCurrPregComplication + ).toBe('keep'); + }); + + it('single None/Nil keeps None enabled and clears other text', () => { + component.resetOtherPregnancyComplication( + [{ pregComplicationType: 'Nil' }], + 0 + ); + expect(component.disableNonePregnancyComplication).toBeFalse(); + expect(component.showAllPregComplication).toBeFalse(); + expect(component.showOtherPregnancyComplication).toBeFalse(); + expect( + component.generalDiagnosisForm.value.otherCurrPregComplication + ).toBeNull(); + component.resetOtherPregnancyComplication( + [{ pregComplicationType: 'None' }], + 0 + ); + expect(component.disableNonePregnancyComplication).toBeFalse(); + }); + + it('single real complication disables None', () => { + component.resetOtherPregnancyComplication( + [{ pregComplicationType: 'Anemia' }], + { otherCurrPregComplication: 'ignored' } + ); + expect(component.disableNonePregnancyComplication).toBeTrue(); + expect( + component.generalDiagnosisForm.value.otherCurrPregComplication + ).toBe('keep'); + }); + + it('empty list shows all and patches other from diagnosis', () => { + component.resetOtherPregnancyComplication([], 0); + expect(component.disableNonePregnancyComplication).toBeFalse(); + expect(component.showAllPregComplication).toBeTrue(); + component.resetOtherPregnancyComplication( + [{ pregComplicationType: 'Other' }], + { otherCurrPregComplication: 'from-diag' } + ); + expect( + component.generalDiagnosisForm.value.otherCurrPregComplication + ).toBe('from-diag'); + }); + }); + + it('displayPositive flags hypothyroidism as HRP', () => { + component.displayPositive([{ pregComplicationType: 'Hypothyroidism' }]); + expect(component.complicationPregHRP).toBe('true'); + component.displayPositive([{ pregComplicationType: 'Anemia' }]); + expect(component.complicationPregHRP).toBe('false'); + }); + + it('ngOnDestroy unsubscribes master data', () => { + const sub = component.nurseMasterDataSubscription; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/covid-diagnosis/covid-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/covid-diagnosis/covid-diagnosis.component.spec.ts index 0d4447a8..c32e688a 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/covid-diagnosis/covid-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/covid-diagnosis/covid-diagnosis.component.spec.ts @@ -19,28 +19,126 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { of } from 'rxjs'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { CovidDiagnosisComponent } from './covid-diagnosis.component'; +import { DoctorService } from '../../../../shared/services'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; describe('CovidDiagnosisComponent', () => { let component: CovidDiagnosisComponent; let fixture: ComponentFixture; + let doctorService: any; + let session: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CovidDiagnosisComponent], - }).compileComponents(); - })); + providers: [ + ...commonTestProviders(), + { + provide: DoctorService, + useValue: autoSpy(DoctorService), + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(CovidDiagnosisComponent, '') + .compileComponents(); - beforeEach(() => { fixture = TestBed.createComponent(CovidDiagnosisComponent); component = fixture.componentInstance; + session = TestBed.inject(SessionStorageService) as any; + doctorService = TestBed.inject(DoctorService) as any; + component.generalDiagnosisForm = new FormGroup({ + doctorDiagnosis: new FormControl(null), + specialistDiagnosis: new FormControl(null), + }); + }); + + it('enables doctor diagnosis for a non-specialist', () => { + session.setItem('designation', 'Doctor'); + fixture.detectChanges(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.specialist).toBeFalse(); + expect(component.doctorDaignosis?.enabled).toBeTrue(); + expect(component.specialistDaignosis?.enabled).toBeTrue(); + }); + + it('enables specialist diagnosis for TC Specialist', () => { + session.setItem('designation', 'TC Specialist'); + fixture.detectChanges(); + expect(component.specialist).toBeTrue(); + expect(component.doctorDaignosis?.disabled).toBeTrue(); + expect(component.specialistDaignosis?.enabled).toBeTrue(); + }); + + it('refreshes language on ngDoCheck', () => { + fixture.detectChanges(); + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('fetches and patches diagnosis in view mode', () => { + session.setItem('beneficiaryRegID', 'B1'); + session.setItem('visitID', 'V1'); + session.setItem('visitCategory', 'COVID-19 Screening'); + doctorService.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { doctorDiagnonsis: 'Covid', specialistDiagnosis: 'S' }, + }, + }) + ); + fixture.detectChanges(); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(doctorService.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'COVID-19 Screening' + ); + expect(component.generalDiagnosisForm.get('doctorDiagnosis')?.value).toBe( + 'Covid' + ); + expect( + component.generalDiagnosisForm.get('specialistDiagnosis')?.value + ).toBe('S'); + }); + + it('ignores responses without diagnosis or with a non-200 status', () => { fixture.detectChanges(); + component.caseRecordMode = 'view'; + doctorService.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.ngOnChanges(); + doctorService.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 500, data: { diagnosis: { doctorDiagnonsis: 'X' } } }) + ); + component.ngOnChanges(); + doctorService.getCaseRecordAndReferDetails.and.returnValue(of(null)); + component.ngOnChanges(); + expect(component.generalDiagnosisForm.value.doctorDiagnosis).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('does not fetch outside view mode and has no ngOnDestroy to release the subscription', () => { + component.caseRecordMode = 'edit'; + component.ngOnChanges(); + expect(doctorService.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + expect(component.diagnosisSubscription).toBeUndefined(); + expect((component as any).ngOnDestroy).toBeUndefined(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/diagnosis.component.spec.ts index 3f3124d7..bfae1422 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/diagnosis.component.spec.ts @@ -20,27 +20,57 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from 'src/testing/test-utils'; import { DiagnosisComponent } from './diagnosis.component'; describe('DiagnosisComponent', () => { let component: DiagnosisComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [DiagnosisComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DiagnosisComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.generalDiagnosisForm = new FormGroup({}); + component.caseRecordMode = 'new'; + }); + + afterEach(() => fixture.destroy()); + + const selectors = [ + 'app-general-opd-diagnosis', + 'app-anc-diagnosis', + 'app-pnc-diagnosis', + 'app-ncd-care-diagnosis', + 'app-covid-diagnosis', + 'app-ncd-screening-diagnosis', + ]; + const rendered = () => + selectors.filter(s => fixture.nativeElement.querySelector(s)); + + [ + ['General OPD', 'app-general-opd-diagnosis'], + ['ANC', 'app-anc-diagnosis'], + ['PNC', 'app-pnc-diagnosis'], + ['NCD care', 'app-ncd-care-diagnosis'], + ['COVID-19 Screening', 'app-covid-diagnosis'], + ['NCD screening', 'app-ncd-screening-diagnosis'], + ].forEach(([cat, sel]) => { + it(`renders only ${sel} for ${cat}`, () => { + component.visitCategory = cat; + fixture.detectChanges(); + expect(rendered()).toEqual([sel]); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('renders no diagnosis for unknown category', () => { + component.visitCategory = 'Cancer Screening'; + fixture.detectChanges(); + expect(rendered()).toEqual([]); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/general-opd-diagnosis/general-opd-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/general-opd-diagnosis/general-opd-diagnosis.component.spec.ts index 953efd5f..3a9fff63 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/general-opd-diagnosis/general-opd-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/general-opd-diagnosis/general-opd-diagnosis.component.spec.ts @@ -20,27 +20,407 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray } from '@angular/forms'; +import { of, Subject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../../../../core/services/confirmation.service'; +import { DoctorService, MasterdataService } from '../../../../shared/services'; import { GeneralOpdDiagnosisComponent } from './general-opd-diagnosis.component'; describe('GeneralOpdDiagnosisComponent', () => { let component: GeneralOpdDiagnosisComponent; let fixture: ComponentFixture; + let doctor: any; + let master: any; + let confirm: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [GeneralOpdDiagnosisComponent], - }).compileComponents(); - })); + const list = () => + component.generalDiagnosisForm.get('provisionalDiagnosisList') as FormArray; - beforeEach(() => { + beforeEach(async () => { + doctor = autoSpy(DoctorService); + master = autoSpy(MasterdataService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [GeneralOpdDiagnosisComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + beneficiaryRegID: 'B1', + visitID: 'V1', + visitCategory: 'General OPD', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: MasterdataService, useValue: master }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(GeneralOpdDiagnosisComponent, '') + .compileComponents(); fixture = TestBed.createComponent(GeneralOpdDiagnosisComponent); component = fixture.componentInstance; + confirm = TestBed.inject(ConfirmationService); + component.generalDiagnosisForm = + component.utils.createGeneralDiagnosisForm(); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('sets language and exposes provisional diagnosis controls', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.provisionalDiagnosisControls.length).toBe(1); + component.generalDiagnosisForm.removeControl('provisionalDiagnosisList'); + expect(component.provisionalDiagnosisControls).toEqual([]); + }); + + describe('ngOnChanges', () => { + it('does nothing outside view mode', () => { + component.caseRecordMode = 'new'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('fetches and patches diagnosis in view mode', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + instruction: 'rest', + provisionalDiagnosisList: [ + { term: 'Fever', conceptID: 'C1' }, + { term: 'Cough', conceptID: 'C2' }, + ], + }, + }, + }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'General OPD' + ); + expect(component.generalDiagnosisForm.value.instruction).toBe('rest'); + expect(list().length).toBe(2); + expect(list().at(1).value).toEqual({ + conceptID: 'C2', + term: 'Cough', + provisionalDiagnosis: 'Cough', + }); + expect( + list().at(0).get('viewProvisionalDiagnosisProvided')?.disabled + ).toBeTrue(); + expect(list().at(0).getRawValue().viewProvisionalDiagnosisProvided).toBe( + 'Fever' + ); + }); + + it('ignores view response without diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(list().at(0).value.term).toBeNull(); + }); + }); + + it('addDiagnosis adds rows until 30 then alerts', () => { + for (let i = 0; i < 29; i++) component.addDiagnosis(); + expect(list().length).toBe(30); + component.addDiagnosis(); + expect(list().length).toBe(30); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + describe('deleteDiagnosis', () => { + const fillValid = (i: number) => + list() + .at(i) + .patchValue({ + term: 'T' + i, + conceptID: 'C' + i, + provisionalDiagnosis: 'T' + i, + }); + + it('removes a valid row after confirmation when multiple', () => { + component.addDiagnosis(); + fillValid(0); + fillValid(1); + component.deleteDiagnosis(0); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBe('T1'); + }); + + it('resets the only valid row after confirmation', () => { + fillValid(0); + component.deleteDiagnosis(0); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBeNull(); + }); + + it('keeps the row when confirmation declined', () => { + confirm.confirm.and.returnValue(of(false)); + fillValid(0); + component.deleteDiagnosis(0); + expect(list().at(0).value.term).toBe('T0'); + }); + + it('removes an invalid row directly when multiple', () => { + component.addDiagnosis(); + fillValid(0); + component.deleteDiagnosis(1); + expect(confirm.confirm).not.toHaveBeenCalled(); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBe('T0'); + }); + + it('replaces the only invalid row', () => { + const first = list().at(0); + component.deleteDiagnosis(0); + expect(list().length).toBe(1); + expect(list().at(0)).not.toBe(first); + }); + }); + + it('checkProvisionalDiagnosisValidity requires term and conceptID', () => { + expect( + component.checkProvisionalDiagnosisValidity({ + value: { term: 'a', conceptID: 'b' }, + }) + ).toBeFalse(); + expect( + component.checkProvisionalDiagnosisValidity({ value: { term: 'a' } }) + ).toBeTrue(); + }); + + it('displayDiagnosis handles strings, objects and nulls', () => { + expect(component.displayDiagnosis('abc')).toBe('abc'); + expect(component.displayDiagnosis({ Term: 'Fever' })).toBe('Fever'); + // reads capitalised Term only + expect(component.displayDiagnosis({ term: 'Fever' })).toBe(''); + expect(component.displayDiagnosis(null)).toBe(''); + }); + + it('onDiagnosisSelected patches the row or clears it', () => { + component.onDiagnosisSelected({ term: 'Fever', conceptID: 'C1' }, 0); + expect(list().at(0).value).toEqual({ + conceptID: 'C1', + term: 'Fever', + provisionalDiagnosis: 'Fever', + viewProvisionalDiagnosisProvided: 'Fever', + }); + component.onDiagnosisSelected(null, 0); + expect(list().at(0).value.term).toBeNull(); + expect(list().at(0).value.conceptID).toBeNull(); + }); + + describe('diagnosis autocomplete', () => { + const res = (items: any[]) => of({ data: { sctMaster: items } }); + + it('resets state for short terms', () => { + component.suggestedDiagnosisList[0] = [{ term: 'x' }]; + component.onDiagnosisInputKeyup('ab', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.lastQueryByIndex[0]).toBe(''); + expect(component.pageByIndex[0]).toBe(0); + component.onDiagnosisInputKeyup(undefined as any, 0); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('fetches the first page for a new term', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue( + res([{ id: 1, term: 'Fever' }]) + ); + component.onDiagnosisInputKeyup(' fev ', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith( + 'fev', + 0 + ); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1, term: 'Fever' }, + ]); + expect(component.pageByIndex[0]).toBe(0); + expect(component.loadingMore[0]).toBeFalse(); + expect(component.noMore[0]).toBeFalse(); + }); + + it('re-fetches same term without resetting the list', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue(res([{ id: 1 }])); + component.onDiagnosisInputKeyup('fev', 0); + component.onDiagnosisInputKeyup('fev', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([{ id: 1 }]); + }); + + it('marks noMore for empty or missing payload', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue(of(null)); + component.onDiagnosisInputKeyup('fev', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.noMore[0]).toBeTrue(); + }); + + it('appends next page with de-duplication', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValues( + res([{ id: 1 }, { code: 'c2' }]), + res([{ id: 1 }, { code: 'c2' }, { term: 't3' }]) + ); + component.onDiagnosisInputKeyup('fev', 0); + component.onAutoNearEnd(0); + expect( + master.searchDiagnosisBasedOnPageNo.calls.mostRecent().args + ).toEqual(['fev', 1]); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { code: 'c2' }, + { term: 't3' }, + ]); + expect(component.pageByIndex[0]).toBe(1); + }); + + it('append works when no previous list exists', () => { + component.lastQueryByIndex[2] = 'abc'; + master.searchDiagnosisBasedOnPageNo.and.returnValue(res([{ id: 5 }])); + component.onAutoNearEnd(2); + expect( + master.searchDiagnosisBasedOnPageNo.calls.mostRecent().args + ).toEqual(['abc', 1]); + expect(component.suggestedDiagnosisList[2]).toEqual([{ id: 5 }]); + }); + + it('does nothing near end without a query or when noMore', () => { + component.onAutoNearEnd(0); + component.lastQueryByIndex[0] = 'abc'; + component.noMore[0] = true; + component.onAutoNearEnd(0); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('queues and chains a request while loading', () => { + const first = new Subject(); + master.searchDiagnosisBasedOnPageNo.and.returnValues( + first.asObservable(), + res([{ id: 2 }]) + ); + component.onDiagnosisInputKeyup('fev', 0); + expect(component.loadingMore[0]).toBeTrue(); + component.onAutoNearEnd(0); + expect(component.wantMore[0]).toBeTrue(); + component.onDiagnosisInputKeyup('fev', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(1); + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + ]); + expect(component.wantMore[0]).toBeFalse(); + }); + + it('ignores stale results', () => { + const first = new Subject(); + master.searchDiagnosisBasedOnPageNo.and.returnValue(first.asObservable()); + component.onDiagnosisInputKeyup('fev', 0); + component.lastQueryByIndex[0] = 'other'; + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.loadingMore[0]).toBeFalse(); + }); + + it('logs on search error and leaves the row loading', () => { + const err = spyOn(console, 'error'); + master.searchDiagnosisBasedOnPageNo.and.returnValue(throwingObs()); + component.onDiagnosisInputKeyup('fev', 0); + expect(err).toHaveBeenCalledWith('Error fetching diagnosis data'); + // complete() never runs after error, so loading flag is not reset + expect(component.loadingMore[0]).toBeTrue(); + }); + }); + + describe('onPanelReady', () => { + let frames: FrameRequestCallback[]; + const panel = (scrollHeight: number, clientHeight: number) => + ({ scrollHeight, clientHeight }) as HTMLElement; + const runFrames = () => { + while (frames.length) frames.shift()!(0); + }; + + beforeEach(() => { + frames = []; + spyOn(window, 'requestAnimationFrame').and.callFake((cb: any) => { + frames.push(cb); + return frames.length; + }); + }); + + it('does nothing when the panel already scrolls', () => { + component.lastQueryByIndex[0] = 'fever'; + component.onPanelReady(0, panel(500, 100)); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('does nothing when there are no more results', () => { + component.lastQueryByIndex[0] = 'fever'; + component.noMore[0] = true; + component.onPanelReady(0, panel(50, 100)); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('does nothing without a 3+ character query', () => { + component.lastQueryByIndex[0] = 'fe'; + component.onPanelReady(0, panel(50, 100)); + expect(window.requestAnimationFrame).not.toHaveBeenCalled(); + }); + + it('prefetches up to 3 extra pages while not scrollable', () => { + component.lastQueryByIndex[0] = 'fever'; + master.searchDiagnosisBasedOnPageNo.and.callFake((_: any, p: number) => + of({ data: { sctMaster: [{ id: p }] } }) + ); + component.onPanelReady(0, panel(50, 100)); + runFrames(); + expect(master.searchDiagnosisBasedOnPageNo.calls.allArgs()).toEqual([ + ['fever', 1], + ['fever', 2], + ['fever', 3], + ]); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ]); + }); + + it('waits a frame while a page is loading', () => { + component.lastQueryByIndex[0] = 'fever'; + component.loadingMore[0] = true; + component.onPanelReady(0, panel(50, 100)); + expect(frames.length).toBe(1); + component.loadingMore[0] = false; + component.noMore[0] = true; + runFrames(); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-care-diagnosis/ncd-care-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-care-diagnosis/ncd-care-diagnosis.component.spec.ts index 88bee0f9..624360e0 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-care-diagnosis/ncd-care-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-care-diagnosis/ncd-care-diagnosis.component.spec.ts @@ -20,27 +20,463 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, of, Subject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services'; +import { DoctorService, MasterdataService } from '../../../../shared/services'; import { NcdCareDiagnosisComponent } from './ncd-care-diagnosis.component'; describe('NcdCareDiagnosisComponent', () => { let component: NcdCareDiagnosisComponent; let fixture: ComponentFixture; + let doctor: any; + let search: jasmine.Spy; + let master$: BehaviorSubject; + let confirm: any; + let routeParams: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [NcdCareDiagnosisComponent], - }).compileComponents(); - })); + const masterData = () => ({ + ncdCareConditions: ['Diabetes', 'Hypertension', 'Other'], + ncdCareTypes: [ + { ncdCareTypeID: 1, ncdCareType: 'Screening' }, + { ncdCareTypeID: 2, ncdCareType: 'Follow-up' }, + ], + }); - beforeEach(() => { + const list = () => + component.generalDiagnosisForm.get('provisionalDiagnosisList') as FormArray; + + const create = (mode = 'new', data: any = masterData()) => { + master$.next(data); fixture = TestBed.createComponent(NcdCareDiagnosisComponent); component = fixture.componentInstance; + component.generalDiagnosisForm = + component.utils.createNCDCareDiagnosisForm(); + component.caseRecordMode = mode; fixture.detectChanges(); + }; + + beforeEach(async () => { + routeParams = { attendant: 'doctor' }; + master$ = new BehaviorSubject(null); + doctor = autoSpy(DoctorService); + search = jasmine + .createSpy('searchDiagnosisBasedOnPageNo') + .and.returnValue(of({ data: { sctMaster: [] } })); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [NcdCareDiagnosisComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + beneficiaryRegID: 'B1', + visitID: 'V1', + visitCategory: 'NCD care', + }, + }), + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { + doctorMasterData$: master$.asObservable(), + searchDiagnosisBasedOnPageNo: search, + }, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + get params() { + return routeParams; + }, + }, + }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(NcdCareDiagnosisComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture?.destroy()); + + describe('ngOnInit', () => { + it('loads master lists and enables NCD condition for doctor', () => { + create(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.ncdCareConditions).toEqual( + masterData().ncdCareConditions + ); + expect(component.ncdCareTypes.length).toBe(2); + expect(component.attendantType).toBe('doctor'); + expect(component.enableNCDCondition).toBeTrue(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('keeps NCD condition disabled for nurse', () => { + routeParams = { attendant: 'nurse' }; + create(); + expect(component.enableNCDCondition).toBeFalse(); + }); + + it('ignores null master data and partial master data', () => { + create('view', null); + expect(component.ncdCareConditions).toBeUndefined(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + master$.next({}); + expect(component.ncdCareTypes).toBeUndefined(); + }); + + it('fetches and patches diagnosis in view mode', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + ncdScreeningConditionArray: ['Diabetes', 'Other'], + ncdScreeningConditionOther: 'Thyroid', + ncdCareType: 'Follow-up', + ncdComplication: 'none', + provisionalDiagnosisList: [ + { term: 'DM', conceptID: 'C1' }, + { term: 'HTN', conceptID: 'C2' }, + ], + }, + }, + }) + ); + create('view'); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'NCD care' + ); + const v = component.generalDiagnosisForm.value; + expect(component.temp).toEqual(['Diabetes', 'Other']); + expect(component.isNcdScreeningConditionOther).toBeTrue(); + expect(v.ncdCareType).toEqual({ + ncdCareTypeID: 2, + ncdCareType: 'Follow-up', + }); + expect(v.ncdScreeningConditionOther).toBe('Thyroid'); + expect(list().length).toBe(2); + expect(list().at(1).value.term).toBe('HTN'); + expect( + list().at(0).get('viewProvisionalDiagnosisProvided')?.disabled + ).toBeTrue(); + }); + + it('patches diagnosis without provisional list or known care type', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { diagnosis: { ncdCareType: 'Unknown', ncdComplication: 'x' } }, + }) + ); + create('view'); + const v = component.generalDiagnosisForm.value; + expect(v.ncdCareType).toBe('Unknown'); + expect(v.ncdComplication).toBe('x'); + expect(component.isNcdScreeningConditionOther).toBeFalse(); + expect(component.temp).toEqual([]); + expect(list().length).toBe(1); + }); + + it('ignores view response without diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 500 }) + ); + create('view'); + expect(component.generalDiagnosisForm.value.ncdComplication).toBeNull(); + }); + }); + + describe('after init', () => { + beforeEach(() => create()); + + it('exposes provisional diagnosis controls', () => { + expect(component.provisionalDiagnosisControls.length).toBe(1); + component.generalDiagnosisForm.removeControl('provisionalDiagnosisList'); + expect(component.provisionalDiagnosisControls).toEqual([]); + }); + + it('patchProvisionalDiagnosisDetails skips empty first diagnosis', () => { + component.patchProvisionalDiagnosisDetails([{ term: '', conceptID: '' }]); + expect(list().at(0).value.term).toBeNull(); + }); + + it('addDiagnosis adds up to 30 then alerts', () => { + for (let i = 0; i < 29; i++) component.addDiagnosis(); + expect(list().length).toBe(30); + component.addDiagnosis(); + expect(list().length).toBe(30); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + describe('deleteDiagnosis', () => { + const fillValid = (i: number) => + list() + .at(i) + .patchValue({ + term: 'T' + i, + conceptID: 'C' + i, + provisionalDiagnosis: 'T' + i, + }); + + it('removes a valid row after confirmation when multiple', () => { + component.addDiagnosis(); + fillValid(0); + fillValid(1); + component.deleteDiagnosis(0, list().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBe('T1'); + }); + + it('resets the only valid row after confirmation', () => { + fillValid(0); + component.deleteDiagnosis(0, list().at(0)); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBeNull(); + }); + + it('keeps the row when declined', () => { + confirm.confirm.and.returnValue(of(false)); + fillValid(0); + component.deleteDiagnosis(0, list().at(0)); + expect(list().at(0).value.term).toBe('T0'); + }); + + it('removes invalid rows directly', () => { + component.addDiagnosis(); + fillValid(0); + component.deleteDiagnosis(1, list().at(1)); + expect(confirm.confirm).not.toHaveBeenCalled(); + expect(list().length).toBe(1); + const only = list().at(0); + list().at(0).reset(); + component.deleteDiagnosis(0, only); + expect(list().length).toBe(1); + expect(list().at(0)).not.toBe(only); + }); + }); + + it('checkProvisionalDiagnosisValidity requires term and conceptID', () => { + expect( + component.checkProvisionalDiagnosisValidity({ + value: { term: 'a', conceptID: 'b' }, + }) + ).toBeFalse(); + expect( + component.checkProvisionalDiagnosisValidity({ + value: { conceptID: 'b' }, + }) + ).toBeTrue(); + }); + + it('changeNcdScreeningCondition toggles Other and stores the array', () => { + component.changeNcdScreeningCondition(null, { + value: ['Diabetes', 'Other'], + }); + expect(component.isNcdScreeningConditionOther).toBeTrue(); + expect(component.temp).toEqual(['Diabetes', 'Other']); + component.generalDiagnosisForm.patchValue({ + ncdScreeningConditionOther: 'x', + }); + component.changeNcdScreeningCondition(null, { value: ['Diabetes'] }); + expect(component.isNcdScreeningConditionOther).toBeFalse(); + expect( + component.generalDiagnosisForm.value.ncdScreeningConditionOther + ).toBeNull(); + expect( + component.generalDiagnosisForm.value.ncdScreeningConditionArray + ).toEqual(['Diabetes']); + component.changeNcdScreeningCondition(null, { value: null }); + expect(component.isNcdScreeningConditionOther).toBeFalse(); + expect(component.temp).toBeNull(); + }); + + it('displayDiagnosis handles strings, objects and nulls', () => { + expect(component.displayDiagnosis('abc')).toBe('abc'); + expect(component.displayDiagnosis({ term: 'Fever' })).toBe('Fever'); + expect(component.displayDiagnosis(null)).toBe(''); + }); + + it('onDiagnosisSelected patches the row or clears it', () => { + const sel = { term: 'Fever', conceptID: 'C1' }; + component.onDiagnosisSelected(sel, 0); + expect(list().at(0).value.viewProvisionalDiagnosisProvided).toBe(sel); + expect(list().at(0).value.conceptID).toBe('C1'); + expect(list().at(0).value.term).toBe('Fever'); + component.onDiagnosisSelected(null, 0); + expect(list().at(0).value.term).toBeNull(); + }); + + describe('diagnosis autocomplete', () => { + const res = (items: any[]) => of({ data: { sctMaster: items } }); + + it('resets state for short terms', () => { + component.suggestedDiagnosisList[0] = [{ term: 'x' }]; + component.onDiagnosisInputKeyup('ab', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.lastQueryByIndex[0]).toBe(''); + component.onDiagnosisInputKeyup(undefined as any, 0); + expect(search).not.toHaveBeenCalled(); + }); + + it('fetches first page and re-fetches the same term without reset', () => { + search.and.returnValue(res([{ id: 1 }])); + component.onDiagnosisInputKeyup(' fev ', 0); + expect(search).toHaveBeenCalledWith('fev', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([{ id: 1 }]); + component.onDiagnosisInputKeyup('fev', 0); + expect(search).toHaveBeenCalledTimes(2); + expect(component.noMore[0]).toBeFalse(); + }); + + it('marks noMore for missing payload', () => { + search.and.returnValue(of(null)); + component.onDiagnosisInputKeyup('fev', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.noMore[0]).toBeTrue(); + }); + + it('appends next page with de-duplication', () => { + search.and.returnValues( + res([{ id: 1 }, { code: 'c2' }]), + res([{ id: 1 }, { code: 'c2' }, { term: 't3' }]) + ); + component.onDiagnosisInputKeyup('fev', 0); + component.onAutoNearEnd(0); + expect(search.calls.mostRecent().args).toEqual(['fev', 1]); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { code: 'c2' }, + { term: 't3' }, + ]); + expect(component.pageByIndex[0]).toBe(1); + }); + + it('append works when no previous list exists', () => { + component.lastQueryByIndex[2] = 'abc'; + search.and.returnValue(res([{ id: 5 }])); + component.onAutoNearEnd(2); + expect(component.suggestedDiagnosisList[2]).toEqual([{ id: 5 }]); + }); + + it('does nothing near end without a query or when noMore', () => { + component.onAutoNearEnd(0); + component.lastQueryByIndex[0] = 'abc'; + component.noMore[0] = true; + component.onAutoNearEnd(0); + expect(search).not.toHaveBeenCalled(); + }); + + it('queues and chains a request while loading', () => { + const first = new Subject(); + search.and.returnValues(first.asObservable(), res([{ id: 2 }])); + component.onDiagnosisInputKeyup('fev', 0); + component.onAutoNearEnd(0); + expect(component.wantMore[0]).toBeTrue(); + component.onDiagnosisInputKeyup('fev', 0); + expect(search).toHaveBeenCalledTimes(1); + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(search).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + ]); + }); + + it('ignores stale results', () => { + const first = new Subject(); + search.and.returnValue(first.asObservable()); + component.onDiagnosisInputKeyup('fev', 0); + component.lastQueryByIndex[0] = 'other'; + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.loadingMore[0]).toBeFalse(); + }); + + it('logs on search error', () => { + const err = spyOn(console, 'error'); + search.and.returnValue(throwingObs()); + component.onDiagnosisInputKeyup('fev', 0); + expect(err).toHaveBeenCalledWith('Error fetching diagnosis data'); + }); + }); + + describe('onPanelReady', () => { + let frames: FrameRequestCallback[]; + const panel = (scrollHeight: number, clientHeight: number) => + ({ scrollHeight, clientHeight }) as HTMLElement; + const runFrames = () => { + while (frames.length) frames.shift()!(0); + }; + + beforeEach(() => { + frames = []; + spyOn(window, 'requestAnimationFrame').and.callFake((cb: any) => { + frames.push(cb); + return frames.length; + }); + }); + + it('skips scrollable panels, exhausted rows and short queries', () => { + component.lastQueryByIndex[0] = 'fever'; + component.onPanelReady(0, panel(500, 100)); + component.noMore[1] = true; + component.lastQueryByIndex[1] = 'fever'; + component.onPanelReady(1, panel(50, 100)); + component.lastQueryByIndex[2] = 'fe'; + component.onPanelReady(2, panel(50, 100)); + expect(search).not.toHaveBeenCalled(); + }); + + it('prefetches up to 3 extra pages while not scrollable', () => { + component.lastQueryByIndex[0] = 'fever'; + search.and.callFake((_: any, p: number) => + of({ data: { sctMaster: [{ id: p }] } }) + ); + component.onPanelReady(0, panel(50, 100)); + runFrames(); + expect(search.calls.allArgs()).toEqual([ + ['fever', 1], + ['fever', 2], + ['fever', 3], + ]); + }); + + it('waits a frame while a page is loading', () => { + component.lastQueryByIndex[0] = 'fever'; + component.loadingMore[0] = true; + component.onPanelReady(0, panel(50, 100)); + expect(frames.length).toBe(1); + component.loadingMore[0] = false; + component.noMore[0] = true; + runFrames(); + expect(search).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-screening-diagnosis/ncd-screening-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-screening-diagnosis/ncd-screening-diagnosis.component.spec.ts index 9a0c6054..f0fc8c5a 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-screening-diagnosis/ncd-screening-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/ncd-screening-diagnosis/ncd-screening-diagnosis.component.spec.ts @@ -20,27 +20,428 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray } from '@angular/forms'; +import { BehaviorSubject, of, Subject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../../../../core/services/confirmation.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../../shared/services'; import { NcdScreeningDiagnosisComponent } from './ncd-screening-diagnosis.component'; describe('NcdScreeningDiagnosisComponent', () => { let component: NcdScreeningDiagnosisComponent; let fixture: ComponentFixture; + let doctor: any; + let master: any; + let confirm: any; + let enableDiag$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [NcdScreeningDiagnosisComponent], - }).compileComponents(); - })); + const list = () => + component.generalDiagnosisForm.get('provisionalDiagnosisList') as FormArray; - beforeEach(() => { + beforeEach(async () => { + doctor = autoSpy(DoctorService); + master = autoSpy(MasterdataService); + enableDiag$ = new BehaviorSubject(false); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [NcdScreeningDiagnosisComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + beneficiaryRegID: 'B1', + visitID: 'V1', + visitCategory: 'NCD screening', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: MasterdataService, useValue: master }, + { + provide: NurseService, + useValue: { enableProvisionalDiag$: enableDiag$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(NcdScreeningDiagnosisComponent, '') + .compileComponents(); fixture = TestBed.createComponent(NcdScreeningDiagnosisComponent); component = fixture.componentInstance; + confirm = TestBed.inject(ConfirmationService); + component.generalDiagnosisForm = + component.utils.createNCDScreeningDiagnosisForm(); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('sets language and exposes provisional diagnosis controls', () => { + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.provisionalDiagnosisControls.length).toBe(1); + component.generalDiagnosisForm.removeControl('provisionalDiagnosisList'); + expect(component.provisionalDiagnosisControls).toEqual([]); + }); + + describe('ngOnChanges', () => { + it('does nothing outside view mode', () => { + component.caseRecordMode = 'new'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('fetches and patches diagnosis in view mode', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + instruction: 'rest', + provisionalDiagnosisList: [ + { term: 'Fever', conceptID: 'C1' }, + { term: 'Cough', conceptID: 'C2' }, + ], + }, + }, + }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'NCD screening' + ); + expect(component.generalDiagnosisForm.value.instruction).toBe('rest'); + expect(list().length).toBe(2); + expect(list().at(1).value).toEqual({ + conceptID: 'C2', + term: 'Cough', + provisionalDiagnosis: 'Cough', + }); + expect( + list().at(0).get('viewProvisionalDiagnosisProvided')?.disabled + ).toBeTrue(); + expect(list().at(0).getRawValue().viewProvisionalDiagnosisProvided).toBe( + 'Fever' + ); + }); + + it('ignores view response without diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(list().at(0).value.term).toBeNull(); + }); + }); + + it('addDiagnosis adds rows until 30 then alerts', () => { + for (let i = 0; i < 29; i++) component.addDiagnosis(); + expect(list().length).toBe(30); + component.addDiagnosis(); + expect(list().length).toBe(30); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + describe('deleteDiagnosis', () => { + const fillValid = (i: number) => + list() + .at(i) + .patchValue({ + term: 'T' + i, + conceptID: 'C' + i, + provisionalDiagnosis: 'T' + i, + }); + + it('removes a valid row after confirmation when multiple', () => { + component.addDiagnosis(); + fillValid(0); + fillValid(1); + component.deleteDiagnosis(0, list().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBe('T1'); + }); + + it('resets the only valid row after confirmation', () => { + fillValid(0); + component.deleteDiagnosis(0, list().at(0)); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBeNull(); + }); + + it('keeps the row when confirmation declined', () => { + confirm.confirm.and.returnValue(of(false)); + fillValid(0); + component.deleteDiagnosis(0, list().at(0)); + expect(list().at(0).value.term).toBe('T0'); + }); + + it('removes an invalid row directly when multiple', () => { + component.addDiagnosis(); + fillValid(0); + component.deleteDiagnosis(1, list().at(1)); + expect(confirm.confirm).not.toHaveBeenCalled(); + expect(list().length).toBe(1); + expect(list().at(0).value.term).toBe('T0'); + }); + + it('replaces the only invalid row', () => { + const first = list().at(0); + component.deleteDiagnosis(0, list().at(0)); + expect(list().length).toBe(1); + expect(list().at(0)).not.toBe(first); + }); + }); + + it('checkProvisionalDiagnosisValidity requires term and conceptID', () => { + expect( + component.checkProvisionalDiagnosisValidity({ + value: { term: 'a', conceptID: 'b' }, + }) + ).toBeFalse(); + expect( + component.checkProvisionalDiagnosisValidity({ value: { term: 'a' } }) + ).toBeTrue(); + }); + + it('displayDiagnosis handles strings, objects and nulls', () => { + expect(component.displayDiagnosis('abc')).toBe('abc'); + expect(component.displayDiagnosis({ term: 'Fever' })).toBe('Fever'); + expect(component.displayDiagnosis(null)).toBe(''); + }); + + it('onDiagnosisSelected patches the row or clears it', () => { + const sel = { term: 'Fever', conceptID: 'C1' }; + component.onDiagnosisSelected(sel, 0); + expect(list().at(0).value.viewProvisionalDiagnosisProvided).toBe(sel); + expect(list().at(0).value.conceptID).toBe('C1'); + expect(list().at(0).value.term).toBe('Fever'); + component.onDiagnosisSelected(null, 0); + expect(list().at(0).value.term).toBeNull(); + expect(list().at(0).value.conceptID).toBeNull(); + }); + + it('tracks enableProvisionalDiag stream', () => { + expect(component.enableProvisionalDiag).toBeFalse(); + enableDiag$.next(3); + expect(component.enableProvisionalDiag).toBeTrue(); + enableDiag$.next(0); + expect(component.enableProvisionalDiag).toBeFalse(); + }); + + it('exposes instruction and provisionalDiagnosis getters', () => { + component.generalDiagnosisForm.patchValue({ instruction: 'rest' }); + expect(component.specialistDaignosis?.value).toBe('rest'); + // form has no top-level provisionalDiagnosis control + expect(component.doctorDaignosis).toBeNull(); + }); + + describe('diagnosis autocomplete', () => { + const res = (items: any[]) => of({ data: { sctMaster: items } }); + + it('resets state for short terms', () => { + component.suggestedDiagnosisList[0] = [{ term: 'x' }]; + component.onDiagnosisInputKeyup('ab', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.lastQueryByIndex[0]).toBe(''); + expect(component.pageByIndex[0]).toBe(0); + component.onDiagnosisInputKeyup(undefined as any, 0); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('fetches the first page for a new term', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue( + res([{ id: 1, term: 'Fever' }]) + ); + component.onDiagnosisInputKeyup(' fev ', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith( + 'fev', + 0 + ); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1, term: 'Fever' }, + ]); + expect(component.pageByIndex[0]).toBe(0); + expect(component.loadingMore[0]).toBeFalse(); + expect(component.noMore[0]).toBeFalse(); + }); + + it('re-fetches same term without resetting the list', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue(res([{ id: 1 }])); + component.onDiagnosisInputKeyup('fev', 0); + component.onDiagnosisInputKeyup('fev', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([{ id: 1 }]); + }); + + it('marks noMore for empty or missing payload', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue(of(null)); + component.onDiagnosisInputKeyup('fev', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.noMore[0]).toBeTrue(); + }); + + it('appends next page with de-duplication', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValues( + res([{ id: 1 }, { code: 'c2' }]), + res([{ id: 1 }, { code: 'c2' }, { term: 't3' }]) + ); + component.onDiagnosisInputKeyup('fev', 0); + component.onAutoNearEnd(0); + expect( + master.searchDiagnosisBasedOnPageNo.calls.mostRecent().args + ).toEqual(['fev', 1]); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { code: 'c2' }, + { term: 't3' }, + ]); + expect(component.pageByIndex[0]).toBe(1); + }); + + it('append works when no previous list exists', () => { + component.lastQueryByIndex[2] = 'abc'; + master.searchDiagnosisBasedOnPageNo.and.returnValue(res([{ id: 5 }])); + component.onAutoNearEnd(2); + expect( + master.searchDiagnosisBasedOnPageNo.calls.mostRecent().args + ).toEqual(['abc', 1]); + expect(component.suggestedDiagnosisList[2]).toEqual([{ id: 5 }]); + }); + + it('does nothing near end without a query or when noMore', () => { + component.onAutoNearEnd(0); + component.lastQueryByIndex[0] = 'abc'; + component.noMore[0] = true; + component.onAutoNearEnd(0); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('queues and chains a request while loading', () => { + const first = new Subject(); + master.searchDiagnosisBasedOnPageNo.and.returnValues( + first.asObservable(), + res([{ id: 2 }]) + ); + component.onDiagnosisInputKeyup('fev', 0); + expect(component.loadingMore[0]).toBeTrue(); + component.onAutoNearEnd(0); + expect(component.wantMore[0]).toBeTrue(); + component.onDiagnosisInputKeyup('fev', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(1); + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + ]); + expect(component.wantMore[0]).toBeFalse(); + }); + + it('ignores stale results', () => { + const first = new Subject(); + master.searchDiagnosisBasedOnPageNo.and.returnValue(first.asObservable()); + component.onDiagnosisInputKeyup('fev', 0); + component.lastQueryByIndex[0] = 'other'; + first.next({ data: { sctMaster: [{ id: 1 }] } }); + first.complete(); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.loadingMore[0]).toBeFalse(); + }); + + it('logs on search error and leaves the row loading', () => { + const err = spyOn(console, 'error'); + master.searchDiagnosisBasedOnPageNo.and.returnValue(throwingObs()); + component.onDiagnosisInputKeyup('fev', 0); + expect(err).toHaveBeenCalledWith('Error fetching diagnosis data'); + // complete() never runs after error, so loading flag is not reset + expect(component.loadingMore[0]).toBeTrue(); + }); + }); + + describe('onPanelReady', () => { + let frames: FrameRequestCallback[]; + const panel = (scrollHeight: number, clientHeight: number) => + ({ scrollHeight, clientHeight }) as HTMLElement; + const runFrames = () => { + while (frames.length) frames.shift()!(0); + }; + + beforeEach(() => { + frames = []; + spyOn(window, 'requestAnimationFrame').and.callFake((cb: any) => { + frames.push(cb); + return frames.length; + }); + }); + + it('does nothing when the panel already scrolls', () => { + component.lastQueryByIndex[0] = 'fever'; + component.onPanelReady(0, panel(500, 100)); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('does nothing when there are no more results', () => { + component.lastQueryByIndex[0] = 'fever'; + component.noMore[0] = true; + component.onPanelReady(0, panel(50, 100)); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('does nothing without a 3+ character query', () => { + component.lastQueryByIndex[0] = 'fe'; + component.onPanelReady(0, panel(50, 100)); + expect(window.requestAnimationFrame).not.toHaveBeenCalled(); + }); + + it('prefetches up to 3 extra pages while not scrollable', () => { + component.lastQueryByIndex[0] = 'fever'; + master.searchDiagnosisBasedOnPageNo.and.callFake((_: any, p: number) => + of({ data: { sctMaster: [{ id: p }] } }) + ); + component.onPanelReady(0, panel(50, 100)); + runFrames(); + expect(master.searchDiagnosisBasedOnPageNo.calls.allArgs()).toEqual([ + ['fever', 1], + ['fever', 2], + ['fever', 3], + ]); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ]); + }); + + it('waits a frame while a page is loading', () => { + component.lastQueryByIndex[0] = 'fever'; + component.loadingMore[0] = true; + component.onPanelReady(0, panel(50, 100)); + expect(frames.length).toBe(1); + component.loadingMore[0] = false; + component.noMore[0] = true; + runFrames(); + expect(master.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.spec.ts index c2bff4ee..5da28950 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/diagnosis/pnc-diagnosis/pnc-diagnosis.component.spec.ts @@ -20,27 +20,386 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from '../../../../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../../../../core/services/confirmation.service'; +import { DoctorService, MasterdataService } from '../../../../shared/services'; import { PncDiagnosisComponent } from './pnc-diagnosis.component'; describe('PncDiagnosisComponent', () => { let component: PncDiagnosisComponent; let fixture: ComponentFixture; + let doctor: any; + let master: any; + let confirm: any; + let beneficiary$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PncDiagnosisComponent], - }).compileComponents(); - })); + const provisional = () => + component.generalDiagnosisForm.controls[ + 'provisionalDiagnosisList' + ] as FormArray; + const confirmatory = () => + component.generalDiagnosisForm.controls[ + 'confirmatoryDiagnosisList' + ] as FormArray; - beforeEach(() => { + beforeEach(async () => { + beneficiary$ = new BehaviorSubject(null); + doctor = autoSpy(DoctorService); + master = autoSpy(MasterdataService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PncDiagnosisComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: 'B1', + visitID: 'V1', + visitCategory: 'PNC', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: MasterdataService, useValue: master }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: beneficiary$ }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PncDiagnosisComponent, '') + .compileComponents(); fixture = TestBed.createComponent(PncDiagnosisComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.generalDiagnosisForm = new FormGroup({ + provisionalDiagnosisList: new FormArray([ + component.utils.initProvisionalDiagnosisList(), + ]), + confirmatoryDiagnosisList: new FormArray([ + component.utils.initConfirmatoryDiagnosisList(), + ]), + isMaternalDeath: new FormControl(null), + placeOfDeath: new FormControl('Home'), + dateOfDeath: new FormControl(null), + causeOfDeath: new FormControl('x'), + }); + confirm = TestBed.inject(ConfirmationService); + component.ngOnInit(); + component.assignSelectedLanguage(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('ngOnInit sets dates and a one-year minimum death date', () => { + expect(component.today).toEqual(jasmine.any(Date)); + const diffDays = + (component.today.getTime() - component.minimumDeathDate.getTime()) / + (24 * 60 * 60 * 1000); + expect(diffDays).toBe(365); + expect(component.beneficiaryAge).toBeUndefined(); + }); + + it('beneficiary details set age and dob year', () => { + beneficiary$.next({ ageVal: 25 }); + expect(component.beneficiaryAge).toBe(25); + expect(component.dob.getFullYear()).toBe( + component.today.getFullYear() - 25 + ); + }); + + it('ngDoCheck refreshes language', () => { + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('control getters return form array controls or empty', () => { + expect(component.provisionalDiagnosisControls.length).toBe(1); + expect(component.confirmatoryDiagnosisControls.length).toBe(1); + expect(component.getConfirmatoryDiagnosisList()?.length).toBe(1); + component.generalDiagnosisForm = new FormGroup({}); + expect(component.provisionalDiagnosisControls).toEqual([]); + expect(component.confirmatoryDiagnosisControls).toEqual([]); + expect(component.getConfirmatoryDiagnosisList()).toBeNull(); + }); + + describe('ngOnChanges (view mode)', () => { + it('fetches and patches diagnosis incl. lists and death date', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + isMaternalDeath: true, + dateOfDeath: '2024-02-01T00:00:00.000Z', + provisionalDiagnosisList: [ + { term: 'Fever', conceptID: 'C1' }, + { term: 'Cough', conceptID: 'C2' }, + ], + confirmatoryDiagnosisList: [ + { term: 'Malaria', conceptID: 'C3' }, + { term: 'Typhoid', conceptID: 'C4' }, + ], + }, + }, + }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'PNC' + ); + expect(component.isMaternalDeath).toBeTrue(); + expect( + component.generalDiagnosisForm.controls['dateOfDeath'].value + ).toEqual(new Date('2024-02-01T00:00:00.000Z')); + expect(provisional().length).toBe(2); + expect(provisional().at(0).value.term).toBe('Fever'); + expect(provisional().at(1).value.provisionalDiagnosis).toBe('Cough'); + expect( + (provisional().at(0) as FormGroup).controls[ + 'viewProvisionalDiagnosisProvided' + ].disabled + ).toBeTrue(); + expect(confirmatory().length).toBe(2); + expect(confirmatory().at(1).getRawValue().confirmatoryDiagnosis).toBe( + 'Typhoid' + ); + expect( + (confirmatory().at(0) as FormGroup).controls['confirmatoryDiagnosis'] + .disabled + ).toBeTrue(); + }); + + it('patches diagnosis without date or lists', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: { diagnosis: { isMaternalDeath: false } } }) + ); + component.caseRecordMode = 'view'; + component.ngOnChanges(); + expect(component.isMaternalDeath).toBeFalse(); + expect(provisional().length).toBe(1); + }); + + it('ignores responses without diagnosis', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 500 }) + ); + component.getDiagnosisDetails('B1', 'V1', 'PNC'); + expect(component.isMaternalDeath).toBeNull(); + }); + + it('does not fetch outside view mode', () => { + component.caseRecordMode = 'new'; + component.ngOnChanges(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + }); + + describe('provisional diagnosis rows', () => { + it('adds rows up to 30 then alerts', () => { + component.addProvisionalDiagnosis(); + expect(provisional().length).toBe(2); + while (provisional().length < 30) component.addProvisionalDiagnosis(); + component.addProvisionalDiagnosis(); + expect(provisional().length).toBe(30); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + it('removes a valid row after confirmation', () => { + component.addProvisionalDiagnosis(); + provisional().at(0).patchValue({ + conceptID: 'C', + term: 'T', + provisionalDiagnosis: 'T', + }); + component.removeProvisionalDiagnosis(0, provisional().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(provisional().length).toBe(1); + expect(component.generalDiagnosisForm.dirty).toBeTrue(); + }); + + it('resets the last valid row after confirmation', () => { + const f: any = provisional().at(0); + f.patchValue({ conceptID: 'C', term: 'T', provisionalDiagnosis: 'T' }); + component.removeProvisionalDiagnosis(0, f); + expect(provisional().length).toBe(1); + expect(f.value.term).toBeNull(); + expect(f.controls.provisionalDiagnosis.enabled).toBeTrue(); + }); + + it('keeps a valid row when confirmation declined', () => { + confirm.confirm.and.returnValue(of(false)); + const f: any = provisional().at(0); + f.patchValue({ conceptID: 'C', term: 'T', provisionalDiagnosis: 'T' }); + component.removeProvisionalDiagnosis(0, f); + expect(f.value.term).toBe('T'); + expect(component.generalDiagnosisForm.dirty).toBeFalse(); + }); + + it('removes or resets invalid rows without confirmation', () => { + component.addProvisionalDiagnosis(); + component.removeProvisionalDiagnosis(1, provisional().at(1)); + expect(provisional().length).toBe(1); + const f: any = provisional().at(0); + f.controls.provisionalDiagnosis.disable(); + component.removeProvisionalDiagnosis(0, f); + expect(f.controls.provisionalDiagnosis.enabled).toBeTrue(); + expect(confirm.confirm).not.toHaveBeenCalled(); + }); + }); + + describe('confirmatory diagnosis rows', () => { + it('adds rows up to 30 then alerts', () => { + while (confirmatory().length < 30) component.addConfirmatoryDiagnosis(); + component.addConfirmatoryDiagnosis(); + expect(confirmatory().length).toBe(30); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + it('removes a valid row after confirmation', () => { + component.addConfirmatoryDiagnosis(); + confirmatory().at(0).patchValue({ conceptID: 'C', term: 'T' }); + component.removeConfirmatoryDiagnosis(0, confirmatory().at(0)); + expect(confirmatory().length).toBe(1); + expect(component.generalDiagnosisForm.dirty).toBeTrue(); + }); + + it('resets the last valid row after confirmation', () => { + const f: any = confirmatory().at(0); + f.patchValue({ conceptID: 'C', term: 'T' }); + f.controls.confirmatoryDiagnosis.disable(); + component.removeConfirmatoryDiagnosis(0, f); + expect(f.value.term).toBeNull(); + expect(f.controls.confirmatoryDiagnosis.enabled).toBeTrue(); + }); + + it('keeps a valid row when confirmation declined', () => { + confirm.confirm.and.returnValue(of(false)); + const f: any = confirmatory().at(0); + f.patchValue({ conceptID: 'C', term: 'T' }); + component.removeConfirmatoryDiagnosis(0, f); + expect(f.value.term).toBe('T'); + }); + + it('removes or resets invalid rows without confirmation', () => { + component.addConfirmatoryDiagnosis(); + component.removeConfirmatoryDiagnosis(1, confirmatory().at(1)); + expect(confirmatory().length).toBe(1); + const f: any = confirmatory().at(0); + f.controls.confirmatoryDiagnosis.disable(); + component.removeConfirmatoryDiagnosis(0, f); + expect(f.controls.confirmatoryDiagnosis.enabled).toBeTrue(); + expect(confirm.confirm).not.toHaveBeenCalled(); + }); + }); + + it('checkWithDeathDetails clears death fields', () => { + component.checkWithDeathDetails(); + const v = component.generalDiagnosisForm.value; + expect(v.placeOfDeath).toBeNull(); + expect(v.dateOfDeath).toBeNull(); + expect(v.causeOfDeath).toBeNull(); + }); + + it('validity checks require term and conceptID', () => { + expect( + component.checkProvisionalDiagnosisValidity({ + value: { term: 'T', conceptID: 'C' }, + }) + ).toBeFalse(); + expect( + component.checkProvisionalDiagnosisValidity({ value: { term: 'T' } }) + ).toBeTrue(); + expect( + component.checkConfirmatoryDiagnosisValidity({ + value: { term: 'T', conceptID: 'C' }, + }) + ).toBeFalse(); + expect( + component.checkConfirmatoryDiagnosisValidity({ value: {} }) + ).toBeTrue(); + }); + + describe('diagnosis search', () => { + it('provisional search queries after 3 chars', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ term: 'Fever' }] } }) + ); + component.onDiagnosisInputKeyup('fev', 0); + expect(master.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith( + 'fev', + 0 + ); + expect(component.suggestedDiagnosisList[0]).toEqual([{ term: 'Fever' }]); + component.onDiagnosisInputKeyup('fe', 0); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + }); + + it('confirmatory search queries after 3 chars', () => { + master.searchDiagnosisBasedOnPageNo.and.returnValue(of(null)); + component.onConfirmatoryDiagnosisInputKeyup('mal', 1); + expect(component.suggestedConfirmatoryDiagnosisList[1]).toBeUndefined(); + component.onConfirmatoryDiagnosisInputKeyup('ma', 1); + expect(component.suggestedConfirmatoryDiagnosisList[1]).toEqual([]); + }); + }); + + it('display helpers handle strings, objects and null', () => { + expect(component.displayDiagnosis('abc')).toBe('abc'); + expect(component.displayDiagnosis({ term: 'T' })).toBe('T'); + expect(component.displayDiagnosis(null)).toBe(''); + expect(component.displayConfirmatoryDiagnosis('x')).toBe('x'); + expect(component.displayConfirmatoryDiagnosis({ term: 'Y' })).toBe('Y'); + expect(component.displayConfirmatoryDiagnosis(undefined)).toBe(''); + }); + + it('onDiagnosisSelected patches provisional row', () => { + const sel = { term: 'Fever', conceptID: 'C1' }; + component.onDiagnosisSelected(sel, 0); + expect(provisional().at(0).value).toEqual({ + conceptID: 'C1', + term: 'Fever', + provisionalDiagnosis: 'Fever', + viewProvisionalDiagnosisProvided: sel, + }); + component.onDiagnosisSelected(null, 0); + expect(provisional().at(0).value.term).toBeNull(); + }); + + it('onConfirmatoryDiagnosisSelected patches confirmatory row', () => { + component.onConfirmatoryDiagnosisSelected( + { term: 'Malaria', conceptID: 'C9' }, + 0 + ); + expect(confirmatory().at(0).value.term).toBe('Malaria'); + expect(confirmatory().at(0).value.conceptID).toBe('C9'); + component.onConfirmatoryDiagnosisSelected(undefined, 0); + expect(confirmatory().at(0).value.conceptID).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes beneficiary subscription', () => { + const s = jasmine.createSpyObj('s', ['unsubscribe']); + component.beneficiaryDetailsSubscription = s; + component.ngOnDestroy(); + expect(s.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/doctor-investigations/doctor-investigations.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/doctor-investigations/doctor-investigations.component.spec.ts index 318f2c64..50045460 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/doctor-investigations/doctor-investigations.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/doctor-investigations/doctor-investigations.component.spec.ts @@ -20,27 +20,376 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; +import { IdrsscoreService } from '../../../shared/services/idrsscore.service'; import { DoctorInvestigationsComponent } from './doctor-investigations.component'; describe('DoctorInvestigationsComponent', () => { let component: DoctorInvestigationsComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let nurse: any; + let idrs: any; + let nurseMaster$: BehaviorSubject; + let doctorMaster$: BehaviorSubject; + let rbs$: BehaviorSubject; + let diabetes$: BehaviorSubject; + let hyper$: BehaviorSubject; + let confirmedDiab$: BehaviorSubject; + let systolic$: BehaviorSubject; + let diastolic$: BehaviorSubject; + + const rbs = { + procedureID: 1, + procedureName: environment.RBSTest, + procedureType: 'Laboratory', + }; + const hb = { + procedureID: 2, + procedureName: environment.haemoglobinTest, + procedureType: 'Laboratory', + }; + const va = { + procedureID: 3, + procedureName: environment.visualAcuityTest, + procedureType: 'Laboratory', + }; + const xray = { + procedureID: 4, + procedureName: 'X-Ray', + procedureType: 'Radiology', + }; + const procedures = [rbs, hb, va, xray]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + nurseMaster$ = new BehaviorSubject(null); + doctorMaster$ = new BehaviorSubject(null); + rbs$ = new BehaviorSubject(null); + diabetes$ = new BehaviorSubject(0); + hyper$ = new BehaviorSubject(0); + confirmedDiab$ = new BehaviorSubject(0); + systolic$ = new BehaviorSubject(null); + diastolic$ = new BehaviorSubject(null); + master = autoSpy(MasterdataService, { + nurseMasterData$: nurseMaster$, + doctorMasterData$: doctorMaster$, + }); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService, { + rbsTestResultCurrent$: rbs$, + rbsTestResultFromDoctorFetch: null, + }); + idrs = autoSpy(IdrsscoreService, { + diabetesSelectedFlag$: diabetes$, + hypertensionSelectedFlag$: hyper$, + confirmedDiabeticSelectedFlag$: confirmedDiab$, + systolicBpValue$: systolic$, + diastolicBpValue$: diastolic$, + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [DoctorInvestigationsComponent], + providers: [ + ...commonTestProviders({ + session: { + visitID: 'v1', + beneficiaryRegID: 'b1', + visitCategory: 'NCD screening', + }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: IdrsscoreService, useValue: idrs }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(DoctorInvestigationsComponent, ''); fixture = TestBed.createComponent(DoctorInvestigationsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.generalDoctorInvestigationForm = new FormGroup({ + labTest: new FormControl(null), + radiologyTest: new FormControl(null), + externalInvestigations: new FormControl(null), + }); + spyOn(console, 'log'); + }); + + afterEach(() => component.ngOnDestroy()); + + describe('ngOnInit', () => { + it('clears IDRS / nurse state and subscribes to flags', () => { + component.ngOnInit(); + expect(idrs.clearDiabetesSelected).toHaveBeenCalled(); + expect(idrs.clearSystolicBp).toHaveBeenCalled(); + expect(idrs.clearDiastolicBp).toHaveBeenCalled(); + expect(idrs.clearHypertensionSelected).toHaveBeenCalled(); + expect(idrs.clearConfirmedDiabeticSelected).toHaveBeenCalled(); + expect(nurse.clearRbsInVitals).toHaveBeenCalled(); + expect(nurse.clearRbsSelectedInInvestigation).toHaveBeenCalled(); + expect(component.diabetesSelected).toBe(0); + expect(component.hypertensionSelected).toBe(0); + expect(component.VisualAcuityMandatory).toBeFalse(); + expect(idrs.clearTMCSuggested).toHaveBeenCalled(); + }); + + it('high systolic BP with no confirmed hypertension makes VA mandatory', () => { + component.ngOnInit(); + systolic$.next(150); + expect(component.systolicBpValue).toBe(150); + expect(component.VisualAcuityMandatory).toBeTrue(); + expect(idrs.setVisualAcuityTestMandatoryFlag).toHaveBeenCalled(); + expect(idrs.setTMCSuggested).toHaveBeenCalled(); + }); + + it('high diastolic BP makes VA mandatory', () => { + component.ngOnInit(); + diastolic$.next(95); + expect(component.VisualAcuityMandatory).toBeTrue(); + diastolic$.next(70); + expect(component.VisualAcuityMandatory).toBeFalse(); + }); + + it('confirmed hypertension (1) suppresses BP-based mandate', () => { + component.ngOnInit(); + hyper$.next(1); + systolic$.next(150); + expect(component.VisualAcuityMandatory).toBeFalse(); + hyper$.next(0); + expect(component.VisualAcuityMandatory).toBeTrue(); + }); + + it('confirmed diabetes re-evaluates the mandate', () => { + component.ngOnInit(); + component.RBSTestScore = 250; + confirmedDiab$.next(1); + expect(component.confirmedDiabeticValue).toBe(1); + expect(component.VisualAcuityMandatory).toBeTrue(); + component.RBSTestScore = 100; + confirmedDiab$.next(0); + expect(component.VisualAcuityMandatory).toBeFalse(); + }); + + it('RBS result from vitals is tracked and scored', () => { + component.ngOnInit(); + rbs$.next(250); + expect(component.RBSTestScoreInVitals).toBe(250); + expect(component.RBSTestDoneInVitals).toBeTrue(); + expect(component.rbsTestResultCurrent).toBe(250); + expect(component.VisualAcuityMandatory).toBeTrue(); + rbs$.next(null); + expect(component.RBSTestDoneInVitals).toBeFalse(); + expect(component.rbsTestResultCurrent).toBeNull(); + expect(component.VisualAcuityMandatory).toBeFalse(); + }); + + it('checkForDiabetesSuspected resets vitals RBS flag when suspected without score', () => { + component.ngOnInit(); + component.diabetesSelected = 1; + component.RBSTestScoreInVitals = null as any; + component.RBSTestDoneInVitals = true; + component.checkForDiabetesSuspected(); + expect(component.RBSTestDoneInVitals).toBeFalse(); + }); + + it('logs doctor master data when present', () => { + component.ngOnInit(); + doctorMaster$.next({ a: 1 }); + expect(console.log).toHaveBeenCalledWith('doctor master', { a: 1 }); + }); + }); + + it('ngDoCheck refreshes language', () => { + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + describe('nurse master data', () => { + it('splits lab/radiology and flags RBS / VA / Hb presence', () => { + component.ngOnInit(); + nurseMaster$.next({ procedures }); + expect(component.nonRadiologyMaster).toEqual([rbs, hb, va]); + expect(component.radiologyMaster).toEqual([xray]); + expect(component.rbsPresent).toBeTrue(); + expect(component.visualAcuityPresent).toBeTrue(); + expect(idrs.rBSPresentInMaster).toHaveBeenCalled(); + expect(idrs.visualAcuityPresentInMaster).toHaveBeenCalled(); + expect(idrs.haemoglobinPresentInMaster).toHaveBeenCalled(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('ignores master data without procedures', () => { + component.ngOnInit(); + nurseMaster$.next({}); + expect(component.nonRadiologyMaster).toBeUndefined(); + }); + + it('in view mode loads and patches investigations and RBS score', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + investigation: { + laboratoryList: [ + { procedureID: 1, procedureName: environment.RBSTest }, + { procedureID: 2, procedureName: environment.haemoglobinTest }, + { procedureID: 3, procedureName: environment.visualAcuityTest }, + { procedureID: 4, procedureName: 'X-Ray' }, + ], + }, + diagnosis: { externalInvestigation: 'CT scan' }, + LabReport: [ + { + procedureName: environment.RBSTest, + componentList: [{ testResultValue: 300 }], + }, + { procedureName: 'Other', componentList: [] }, + ], + }, + }) + ); + component.caseRecordMode = 'view'; + component.ngOnInit(); + nurseMaster$.next({ procedures }); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'b1', + 'v1', + 'NCD screening' + ); + const v = component.generalDoctorInvestigationForm.value; + expect(v.labTest).toEqual([rbs, hb, va]); + expect(v.radiologyTest).toEqual([xray]); + expect(v.externalInvestigations).toBe('CT scan'); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + expect(component.hemoglobbinSelected).toBeTrue(); + expect(component.VisualAcuityTestDone).toBeTrue(); + expect(nurse.setRbsSelectedInInvestigation).toHaveBeenCalledWith(true); + expect(component.RBSTestScore).toBe(300); + expect(component.VisualAcuityMandatory).toBeTrue(); + }); + + it('ignores view-mode responses without investigation', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getInvestigationDetails('b1', 'v1', 'x'); + doctor.getCaseRecordAndReferDetails.and.returnValue(of(null)); + component.getInvestigationDetails('b1', 'v1', 'x'); + expect(component.generalDoctorInvestigationForm.value.labTest).toBeNull(); + }); + }); + + it('patchInvestigationDetails without lab list or diagnosis patches empties', () => { + component.patchInvestigationDetails({}, null); + expect(component.generalDoctorInvestigationForm.value).toEqual({ + labTest: [], + radiologyTest: [], + externalInvestigations: '', + }); + expect(component.previousLabTestList).toBeUndefined(); + }); + + describe('canDisable', () => { + it('disables RBS when a current result exists', () => { + component.rbsTestResultCurrent = 100; + expect(component.canDisable({ ...rbs })).toBeTrue(); + }); + + it('disables RBS when doctor-fetch result exists', () => { + nurse.rbsTestResultFromDoctorFetch = 120; + expect(component.canDisable({ ...rbs })).toBeTrue(); + }); + + it('disables tests that were previously prescribed', () => { + component.previousLabTestList = [{ procedureID: 2 }]; + const t1: any = { ...hb }; + const t2: any = { ...va }; + expect(component.canDisable(t1)).toBeTrue(); + expect(t1.disabled).toBeTrue(); + expect(component.canDisable(t2)).toBeFalse(); + expect(t2.disabled).toBeFalse(); + }); + + it('returns false with no previous list', () => { + expect(component.canDisable({ ...hb })).toBeFalse(); + }); + }); + + describe('checkTestName', () => { + it('flags RBS, Hb and VA when selected', () => { + component.checkTestName({ value: [rbs, hb, va] }); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + expect(component.hemoglobbinSelected).toBeTrue(); + expect(component.VisualAcuityTestDone).toBeTrue(); + expect(nurse.setRbsSelectedInInvestigation.calls.allArgs()).toEqual([ + [false], + [true], + ]); + }); + + it('clears flags when none selected', () => { + component.VisualAcuityTestDone = true; + component.checkTestName({ value: [xray] }); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + expect(component.hemoglobbinSelected).toBeFalse(); + expect(component.VisualAcuityTestDone).toBeFalse(); + }); + }); + + it('changeOdDiastolicBp uses the passed value', () => { + component.hypertensionSelected = 0; + component.changeOdDiastolicBp(100); + expect(component.VisualAcuityMandatory).toBeTrue(); + component.changeOdDiastolicBp(80); + expect(component.VisualAcuityMandatory).toBeFalse(); + expect(idrs.clearVisualAcuityTestMandatoryFlag).toHaveBeenCalled(); + }); + + it('changeOfConfirmedHypertension uses the passed value', () => { + component.systolicBpValue = 150; + component.changeOfConfirmedHypertension(0); + expect(component.VisualAcuityMandatory).toBeTrue(); + component.changeOfConfirmedHypertension(1); + expect(component.VisualAcuityMandatory).toBeFalse(); + component.diastolicBpValue = 95; + component.changeOfConfirmedHypertension(0); + expect(component.VisualAcuityMandatory).toBeTrue(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes subscriptions', () => { + component.ngOnInit(); + const names = [ + 'nurseMasterDataSubscription', + 'doctorMasterDataSubscription', + 'investigationSubscription', + 'diabestesSuspectedSubscription', + 'hyperSuspectedSubscription', + 'systolicSubscription', + 'diastolicSubscription', + 'rbsTestResultSubscription', + ]; + const spies = names.map(n => { + const s = jasmine.createSpyObj(n, ['unsubscribe']); + (component as any)[n] = s; + return s; + }); + component.ngOnDestroy(); + spies.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/findings/findings.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/findings/findings.component.spec.ts index f79197af..b2048a6d 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/findings/findings.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/findings/findings.component.spec.ts @@ -20,27 +20,432 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { ConfirmationService } from '../../../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { FindingsComponent } from './findings.component'; describe('FindingsComponent', () => { let component: FindingsComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let nurse: any; + let confirm: any; + let tracking: any; + let nurseMasterData$: BehaviorSubject; + let beneficiary$: BehaviorSubject; + const originalOffline = environment.isMMUOfflineSync; + + const fever = { chiefComplaint: 'Fever', chiefComplaintID: 1 }; + const cough = { chiefComplaint: 'Cough', chiefComplaintID: 2 }; + const headache = { chiefComplaint: 'Headache', chiefComplaintID: 3 }; + const masterList = () => [cough, fever, headache]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const row = (fb: FormBuilder) => + fb.group({ + chiefComplaint: null, + chiefComplaintID: null, + duration: null, + conceptID: null, + unitOfDuration: null, + description: null, + }); + + beforeEach(async () => { + nurseMasterData$ = new BehaviorSubject(null); + beneficiary$ = new BehaviorSubject({ age: '2 years' }); + master = autoSpy(MasterdataService, { nurseMasterData$ }); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [FindingsComponent], + providers: [ + ...commonTestProviders({ + session: { + visitID: 'v1', + beneficiaryRegID: 'b1', + visitCategory: 'General OPD', + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: beneficiary$ }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(FindingsComponent, ''); fixture = TestBed.createComponent(FindingsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + tracking = TestBed.inject(AmritTrackingService); + const fb = TestBed.inject(FormBuilder); + component.generalFindingsForm = new FormGroup({ + complaints: new FormArray([row(fb)]), + clinicalObservation: new FormControl(null), + }); + spyOn(console, 'log'); + component.ngOnInit(); + component.assignSelectedLanguage(); + nurseMasterData$.next({ chiefComplaintMaster: masterList() }); + }); + + afterEach(() => { + environment.isMMUOfflineSync = originalOffline; + }); + + const complaints = () => + component.generalFindingsForm.controls['complaints'] as FormArray; + const addRowAfter = (first: any) => { + complaints().at(0).patchValue({ chiefComplaint: first }); + component.addCheifComplaint(); + }; + const pick = (value: any) => ({ option: { value } }); + + it('ngOnInit loads masters and beneficiary and clears provision', () => { + expect(component.chiefComplaintMaster).toEqual(masterList()); + expect(component.chiefComplaintTemporarayList[0]).toEqual(masterList()); + expect(component.beneficiary).toEqual({ age: '2 years' }); + expect(nurse.clearNCDScreeningProvision).toHaveBeenCalled(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + expect(component.getComplaints()?.length).toBe(1); + }); + + it('getComplaints / getComplaintsList return null without arrays', () => { + expect(component.getComplaintsList()).toBeNull(); + component.generalFindingsForm = new FormGroup({ + complaint: new FormArray([]), + }); + expect(component.getComplaints()).toBeNull(); + expect(component.getComplaintsList()).toEqual([]); + }); + + it('ngDoCheck refreshes language', () => { + component.current_language_set = null; + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + describe('view mode', () => { + it('loads findings, fills the table and removes used complaints', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + findings: { + complaints: [ + { chiefComplaint: 'Fever' }, + { chiefComplaint: 'Xyz' }, + ], + clinicalObservation: 'obs', + }, + }, + }) + ); + component.caseRecordMode = 'view'; + nurseMasterData$.next({ chiefComplaintMaster: masterList() }); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'b1', + 'v1', + 'General OPD' + ); + expect(component.complaintList.length).toBe(2); + expect(component.dataSource.data.length).toBe(2); + expect(component.chiefComplaintMaster).toEqual([cough, headache]); + expect(component.chiefComplaintTemporarayList[0]).toEqual([ + cough, + headache, + ]); + expect(component.generalFindingsForm.value.clinicalObservation).toBe( + 'obs' + ); + }); + + it('ignores responses without findings', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getFindingDetails('b1', 'v1', 'x'); + doctor.getCaseRecordAndReferDetails.and.returnValue(of(null)); + component.getFindingDetails('b1', 'v1', 'x'); + expect(component.complaintList).toEqual([]); + }); + }); + + it('ignores null master data', () => { + component.chiefComplaintMaster = 'unchanged'; + nurseMasterData$.next(null); + expect(component.chiefComplaintMaster).toBe('unchanged'); + }); + + describe('getSCTid', () => { + it('patches concept id on success only', () => { + master.getSnomedCTRecord.and.returnValue( + of({ statusCode: 200, data: { conceptID: 'C1' } }) + ); + component.getSCTid(pick(fever), 0); + expect(master.getSnomedCTRecord).toHaveBeenCalledWith('Fever'); + expect(complaints().at(0).value.conceptID).toBe('C1'); + master.getSnomedCTRecord.and.returnValue(of({ statusCode: 500 })); + component.getSCTid(pick(fever), 0); + expect(complaints().at(0).value.conceptID).toBe('C1'); + }); + + it('swallows errors', () => { + master.getSnomedCTRecord.and.returnValue(throwingObs()); + component.getSCTid(pick(fever), 0); + expect(complaints().at(0).value.conceptID).toBeNull(); + }); + }); + + describe('filterComplaints', () => { + it('selects complaint, removes it from other lists and sets fever flags', () => { + addRowAfter(cough); + component.filterComplaints(pick(fever), 0); + expect(component.selectedChiefComplaintList[0]).toBe(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(fever); + expect(component.chiefComplaintTemporarayList[0]).toContain(fever); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('re-adds the previous selection to other lists when changed', () => { + addRowAfter(cough); + component.filterComplaints(pick(fever), 0); + component.filterComplaints(pick(headache), 0); + expect(component.chiefComplaintTemporarayList[1]).toContain(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(headache); + expect(component.selectedChiefComplaintList[0]).toBe(headache); + }); + + it('unknown complaint leaves selection empty', () => { + component.filterComplaints(pick({ chiefComplaint: 'Fe' }), 0); + expect(component.selectedChiefComplaintList.length).toBe(0); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(false); + }); + }); + + describe('setTempvalidation', () => { + it('fever complaint sets NCD temp and lung assessment', () => { + complaints().at(0).patchValue({ chiefComplaint: fever }); + component.setTempvalidation(); + expect(component.enableProvisionalDiag).toBeTrue(); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(true); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(true); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('offline sync flags respiratory complaints', () => { + environment.isMMUOfflineSync = true; + complaints() + .at(0) + .patchValue({ chiefComplaint: { chiefComplaint: 'Dry Cough' } }); + component.setTempvalidation(); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(true); + }); + + it('offline sync ignores non-respiratory complaints', () => { + environment.isMMUOfflineSync = true; + complaints().at(0).patchValue({ chiefComplaint: headache }); + component.setTempvalidation(); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('online mode does not flag cough', () => { + environment.isMMUOfflineSync = false; + complaints().at(0).patchValue({ chiefComplaint: cough }); + component.setTempvalidation(); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + }); + + it('handles an empty complaints array', () => { + complaints().clear(); + component.setTempvalidation(); + expect(component.enableProvisionalDiag).toBeFalse(); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(false); + }); + }); + + it('addCheifComplaint adds a row with a list excluding chosen complaints', () => { + complaints().at(0).patchValue({ chiefComplaint: fever }); + component.addCheifComplaint(); + expect(complaints().length).toBe(2); + expect(component.chiefComplaintTemporarayList[1]).toEqual([ + cough, + headache, + ]); + }); + + it('addCheifComplaint does not add a list when every complaint is used', () => { + component.chiefComplaintMaster = [fever]; + complaints().at(0).patchValue({ chiefComplaint: fever }); + component.addCheifComplaint(); + expect(complaints().length).toBe(2); + expect(component.chiefComplaintTemporarayList.length).toBe(1); + }); + + describe('removeCheifComplaint', () => { + it('removes a row and restores complaint to other lists', () => { + addRowAfter(fever); + component.filterComplaints(pick(fever), 0); + component.suggestedChiefComplaintList[0] = [fever]; + component.removeCheifComplaint(0, complaints().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(complaints().length).toBe(1); + expect(component.generalFindingsForm.dirty).toBeTrue(); + expect(component.selectedChiefComplaintList[0]).toBeNull(); + expect(component.suggestedChiefComplaintList[0]).toBeNull(); + expect(component.chiefComplaintTemporarayList[1]).toContain(fever); + }); + + it('resets the last remaining row instead of removing it', () => { + const f = complaints().at(0); + f.patchValue({ description: 'x' }); + component.removeCheifComplaint(0, f); + expect(complaints().length).toBe(1); + expect(f.value.description).toBeNull(); + }); + + it('does nothing when not confirmed', () => { + confirm.confirm.and.returnValue(of(false)); + addRowAfter(fever); + component.removeCheifComplaint(0, complaints().at(0)); + expect(complaints().length).toBe(2); + expect(component.generalFindingsForm.dirty).toBeFalse(); + }); + }); + + describe('validateDuration', () => { + it('alerts and clears when duration exceeds age', () => { + const f = complaints().at(0); + f.patchValue({ duration: 5, unitOfDuration: 'Years' }); + component.validateDuration(f); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.DurationAge + ); + expect(f.value.duration).toBeNull(); + expect(f.value.unitOfDuration).toBeNull(); + }); + + it('accepts duration within age or incomplete input', () => { + const f = complaints().at(0); + f.patchValue({ duration: 3, unitOfDuration: 'Days' }); + component.validateDuration(f); + f.patchValue({ duration: 3, unitOfDuration: null }); + component.validateDuration(f); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(f.value.duration).toBe(3); + }); + }); + + it('displayChiefComplaint returns name or undefined', () => { + expect(component.displayChiefComplaint(fever)).toBe('Fever'); + expect(component.displayChiefComplaint(null)).toBeUndefined(); + }); + + describe('suggestChiefComplaintList', () => { + it('string input filters suggestions', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: 'he' }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([headache]); + expect(f.value.chiefComplaint).toBe('he'); + }); + + it('object input filters by its name', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: cough }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([cough]); + }); + + it('no-match string resets the form', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: 'zzz', description: 'd' }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([]); + expect(f.value.chiefComplaint).toBeNull(); + expect(f.value.description).toBeNull(); + }); + + it('null temp list skips filtering', () => { + const f = complaints().at(0); + component.chiefComplaintTemporarayList = null; + component.suggestedChiefComplaintList[0] = [fever]; + f.patchValue({ chiefComplaint: 'he' }); + component.suggestChiefComplaintList(f, 0); + f.patchValue({ chiefComplaint: fever }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([fever]); + }); + }); + + it('sortChiefComplaintList sorts by name', () => { + const list = [headache, fever, cough, { ...fever }]; + component.sortChiefComplaintList(list); + expect(list.map(x => x.chiefComplaint)).toEqual([ + 'Cough', + 'Fever', + 'Fever', + 'Headache', + ]); + }); + + it('checkComplaintFormValidity is false only when all fields filled', () => { + expect( + component.checkComplaintFormValidity({ + value: { chiefComplaint: fever, duration: 2, unitOfDuration: 'Days' }, + }) + ).toBeFalse(); + expect( + component.checkComplaintFormValidity({ + value: { chiefComplaint: fever, duration: 2 }, + }) + ).toBeTrue(); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('duration'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'duration', + 'Findings' + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes subscriptions', () => { + const a = jasmine.createSpyObj('a', ['unsubscribe']); + const b = jasmine.createSpyObj('b', ['unsubscribe']); + component.doctorMasterDataSubscription = a; + component.beneficiaryDetailSubscription = b; + component.ngOnDestroy(); + expect(a.unsubscribe).toHaveBeenCalled(); + expect(b.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/general-case-record.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/general-case-record.component.spec.ts index 4db8b033..a1b21e6f 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/general-case-record.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/general-case-record.component.spec.ts @@ -19,28 +19,76 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; import { GeneralCaseRecordComponent } from './general-case-record.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('GeneralCaseRecordComponent', () => { let component: GeneralCaseRecordComponent; let fixture: ComponentFixture; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralCaseRecordComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GeneralCaseRecordComponent); component = fixture.componentInstance; + form = new FormGroup({ + generalFindingsForm: new FormGroup({ a: new FormControl() }), + generalDiagnosisForm: new FormGroup({ b: new FormControl() }), + generalDoctorInvestigationForm: new FormGroup({ c: new FormControl() }), + drugPrescriptionForm: new FormGroup({ d: new FormControl() }), + }); + component.generalCaseRecordForm = form; fixture.detectChanges(); }); - it('should create', () => { + it('should create and set language', () => { expect(component).toBeTruthy(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('extracts sub forms from the case record form on ngDoCheck', () => { + component.ngDoCheck(); + expect(component.generalFindingsForm).toBe( + form.get('generalFindingsForm') as FormGroup + ); + expect(component.generalDiagnosisForm).toBe( + form.get('generalDiagnosisForm') as FormGroup + ); + expect(component.generalDoctorInvestigationForm).toBe( + form.get('generalDoctorInvestigationForm') as FormGroup + ); + expect(component.drugPrescriptionForm).toBe( + form.get('drugPrescriptionForm') as FormGroup + ); + }); + + it('renders every case-record section', () => { + const el: HTMLElement = fixture.nativeElement; + [ + 'app-previous-significiant-findings', + 'app-previous-visit-details', + 'app-findings', + 'app-diagnosis', + 'app-doctor-investigations', + 'app-prescription', + 'app-test-and-radiology', + ].forEach(sel => + expect(el.querySelector(sel)).withContext(sel).not.toBeNull() + ); + expect(el.textContent).toContain(LANGUAGE_EN.casesheet.prescribe); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.spec.ts index 7f450c5d..54452145 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/prescription/prescription.component.spec.ts @@ -20,27 +20,602 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { MatSelectModule } from '@angular/material/select'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { DoctorService, MasterdataService } from '../../../shared/services'; import { PrescriptionComponent } from './prescription.component'; describe('PrescriptionComponent', () => { let component: PrescriptionComponent; let fixture: ComponentFixture; + let doctor: any; + let master$: BehaviorSubject; + let confirm: any; + let tracking: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PrescriptionComponent], - }).compileComponents(); - })); + const masterData = () => ({ + drugFormMaster: [ + { itemFormID: 1, itemFormName: 'Tablet' }, + { itemFormID: 2, itemFormName: 'Syrup' }, + ], + itemMaster: [ + { + id: 10, + itemID: 100, + itemName: 'Paracetamol', + itemFormID: 1, + strength: '500', + unitOfMeasurement: 'mg', + quantityInHand: 20, + sctCode: 'S1', + sctTerm: 'T1', + }, + { + id: 11, + itemID: 101, + itemName: 'Cough Syrup', + itemFormID: 2, + strength: '5', + unitOfMeasurement: 'ml', + quantityInHand: 3, + }, + ], + NonEdlMaster: [ + { itemID: 200, itemName: 'Pantoprazole', itemFormID: 1, strength: '40' }, + ], + drugDoseMaster: [ + { itemFormID: 1, drugDose: '1 tab' }, + { itemFormID: 2, drugDose: '5 ml' }, + ], + drugFrequencyMaster: [{ frequency: 'OD' }], + drugDurationUnitMaster: [{ drugDuration: 'Days' }], + routeOfAdmin: [{ routeName: 'Oral' }], + }); + + const drugForm = (drugs: any[] = []) => + new FormBuilder().group({ + prescribedDrugs: new FormBuilder().array( + drugs.map(d => new FormBuilder().group(d)) + ), + }); - beforeEach(() => { + const drugsArray = () => + component.drugPrescriptionForm.get('prescribedDrugs') as FormArray; + + const create = ( + mode = 'new', + form: FormGroup = drugForm(), + data: any = null + ) => { + master$.next(data); fixture = TestBed.createComponent(PrescriptionComponent); component = fixture.componentInstance; + component.caseRecordMode = mode; + component.drugPrescriptionForm = form; fixture.detectChanges(); + }; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MatSelectModule], + declarations: [PrescriptionComponent], + providers: [ + ...commonTestProviders({ + session: { + userName: 'drUser', + beneficiaryRegID: 5, + visitID: 6, + visitCategory: 'General OPD', + serviceLineDetails: JSON.stringify({ + vanID: 7, + parkingPlaceID: 8, + }), + }, + }), + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { doctorMasterData$: master$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + // The full template needs the whole Material form-field/autocomplete stack; + // keep the #prescriptionForm ViewChild and the prescribed-drug list markup. + .overrideTemplate( + PrescriptionComponent, + `
+
+ + + {{ drug.value.drugName }} {{ drug.value.drugStrength }} + {{ drug.value.isEDL ? '' : current_language_set?.nonEDLMedicine }} + + +
` + ) + .compileComponents(); + confirm = TestBed.inject(ConfirmationService); + tracking = TestBed.inject(AmritTrackingService); + }); + + afterEach(() => fixture?.destroy()); + + describe('initialisation', () => { + it('sets user, page limits, duration master and language', () => { + create(); + expect(component.createdBy).toBe('drUser'); + expect(component.pageLimits).toEqual([0, 5]); + expect(component.drugDurationMaster.length).toBe(29); + expect(component.drugDurationMaster[0]).toBe(1); + expect(component.drugDurationMaster[28]).toBe(29); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('does nothing with masters while master data is null', () => { + create(); + expect(component.drugMaster).toBeUndefined(); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('assigns masters without fetching in new mode', () => { + create('new', drugForm(), masterData()); + expect(component.drugFormMaster.length).toBe(2); + expect(component.drugMaster.length).toBe(2); + expect(component.drugRouteMaster).toEqual([{ routeName: 'Oral' }]); + expect(component.edlMaster.length).toBe(1); + expect(component.drugFrequencyMaster).toEqual([{ frequency: 'OD' }]); + expect(component.drugDurationUnitMaster).toEqual([ + { drugDuration: 'Days' }, + ]); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('fetches and patches prescription in view mode', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + prescription: [ + { + id: 1, + drugID: 100, + drugName: 'Paracetamol', + drugStrength: '500', + drugUnit: 'mg', + duration: 3, + unit: 'Days', + }, + { id: 2, drugID: 101, drugName: 'Syrup', drugStrength: '5' }, + ], + }, + }) + ); + create('view', drugForm(), masterData()); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 5, + 6, + 'General OPD' + ); + const arr = drugsArray(); + expect(arr.length).toBe(2); + // inserted at 0 each time, so order is reversed + expect(arr.at(0).value.id).toBe(2); + expect(arr.at(0).value.drugStrength).toBe('5'); + expect(arr.at(1).value.drugStrength).toBe('500mg'); + expect(arr.at(1).value.durationView).toBe('3 Days'); + expect(arr.at(1).value.vanID).toBe(7); + expect(arr.at(1).value.parkingPlaceID).toBe(8); + }); + + it('ignores view-mode responses without prescription', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + create('view', drugForm(), masterData()); + expect(drugsArray().length).toBe(0); + }); + + it('unsubscribes master data on destroy', () => { + create(); + const sub = component.doctorMasterDataSubscription; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('ngOnDestroy tolerates missing subscription', () => { + create(); + component.doctorMasterDataSubscription.unsubscribe(); + component.doctorMasterDataSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('helpers', () => { + beforeEach(() => create('new', drugForm(), masterData())); + + it('getPrescribedDrugs returns controls or null', () => { + expect(component.getPrescribedDrugs()).toEqual([]); + component.drugPrescriptionForm = new FormGroup({}); + expect(component.getPrescribedDrugs()).toBeNull(); + }); + + it('displayFn formats options', () => { + expect(component.displayFn(null)).toBe(''); + expect( + component.displayFn({ + itemName: 'Para', + strength: '500', + unitOfMeasurement: 'mg', + quantityInHand: 4, + }) + ).toBe('Para 500mg(4)'); + expect(component.displayFn({ itemName: 'X', strength: '1' })).toBe('X 1'); + }); + + it('trackFieldInteraction delegates to tracking service', () => { + component.trackFieldInteraction('Form'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Form', + 'Prescription' + ); + }); + + it('setLimits computes the page window', () => { + component.setLimits(2); + expect(component.pageLimits).toEqual([10, 15]); + component.setLimits(); + expect(component.pageLimits).toEqual([0, 5]); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('form / drug filtering', () => { + beforeEach(() => create('new', drugForm(), masterData())); + + it('getFormValueChanged resets details and filters by form', () => { + component.tempDrugName = 'old'; + component.currentPrescription.dose = '2'; + component.isStockAvalable = 'primary'; + component.currentPrescription.formName = 'Tablet'; + component.getFormValueChanged(); + expect(component.tempDrugName).toBeNull(); + expect(component.currentPrescription.dose).toBeNull(); + expect(component.isStockAvalable).toBe(''); + expect(component.currentPrescription.formID).toBe(1); + expect(component.filteredDrugMaster.map((d: any) => d.itemName)).toEqual([ + 'Paracetamol', + 'Pantoprazole', + ]); + expect(component.filteredDrugMaster[0].isEDL).toBeTrue(); + expect(component.filteredDrugMaster[1].quantityInHand).toBe(0); + expect(component.subFilteredDrugMaster).toBe( + component.filteredDrugMaster + ); + expect(component.filteredDrugDoseMaster).toEqual([ + { itemFormID: 1, drugDose: '1 tab' }, + ]); + }); + + it('filterMedicine narrows by prefix, case-insensitive', () => { + component.currentPrescription.formName = 'Tablet'; + component.getFormDetails(); + component.filterMedicine('PAN'); + expect( + component.subFilteredDrugMaster.map((d: any) => d.itemName) + ).toEqual(['Pantoprazole']); + component.filterMedicine(''); + expect(component.subFilteredDrugMaster.length).toBe(2); + }); + }); + + describe('reEnterMedicine', () => { + beforeEach(() => create('new', drugForm(), masterData())); + + it('restores selected drug object when a drug is chosen', () => { + component.tempDrugName = 'typed'; + Object.assign(component.currentPrescription, { + id: 1, + drugID: 100, + drugName: 'Paracetamol', + quantity: 20, + sctCode: 'S', + sctTerm: 'T', + drugStrength: '500', + drugUnit: 'mg', + }); + component.reEnterMedicine(); + expect(component.tempDrugName).toEqual({ + id: 1, + itemName: 'Paracetamol', + itemID: 100, + quantityInHand: 20, + sctCode: 'S', + sctTerm: 'T', + strength: '500', + unitOfMeasurement: 'mg', + }); + }); + + it('clears typed text when no drug is chosen', () => { + component.tempDrugName = 'typed'; + component.currentPrescription.drugID = null; + component.reEnterMedicine(); + expect(component.tempDrugName).toBe(''); + }); + + it('resets and re-filters when nothing typed', () => { + component.tempDrugName = ''; + component.currentPrescription.formName = 'Syrup'; + component.reEnterMedicine(); + expect(component.tempDrugName).toBeNull(); + expect(component.currentPrescription.formID).toBe(2); + expect(component.filteredDrugMaster.length).toBe(1); + }); + }); + + describe('selectMedicineObject', () => { + const option = (over: any = {}) => ({ + option: { + value: { + id: 10, + itemID: 100, + itemName: 'Paracetamol', + quantityInHand: 20, + sctCode: 'S1', + sctTerm: 'T1', + strength: '500', + unitOfMeasurement: 'mg', + isEDL: true, + ...over, + }, + }, + }); + + beforeEach(() => create('new', drugForm(), masterData())); + + it('ignores empty selection', () => { + component.selectMedicineObject({ option: { value: null } }); + expect(component.currentPrescription.drugID).toBeNull(); + }); + + it('fills details and marks stock available', () => { + component.selectMedicineObject(option()); + expect(component.currentPrescription.drugID).toBe(100); + expect(component.currentPrescription.drugName).toBe('Paracetamol'); + expect(component.currentPrescription.drugUnit).toBe('mg'); + expect(component.currentPrescription.isEDL).toBeTrue(); + expect(component.isStockAvalable).toBe('primary'); + expect(confirm.confirm).not.toHaveBeenCalled(); + }); + + it('asks confirmation for zero stock and keeps drug on yes', () => { + component.selectMedicineObject( + option({ quantityInHand: 0, isEDL: false }) + ); + expect(confirm.confirm).toHaveBeenCalledWith( + 'info ' + LANGUAGE_EN.nonEDLMedicine, + LANGUAGE_EN.stockNotAvailableWouldYouPrescribe + ' Paracetamol 500mg' + ); + expect(component.isStockAvalable).toBe('warn'); + expect(component.currentPrescription.drugID).toBe(100); + }); + + it('clears drug for zero stock EDL item when declined', () => { + confirm.confirm.and.returnValue(of(false)); + component.tempDrugName = 'Para'; + component.selectMedicineObject(option({ quantityInHand: 0 })); + expect(confirm.confirm.calls.mostRecent().args[0]).toBe('info '); + expect(component.tempDrugName).toBe(''); + expect(component.currentPrescription.drugID).toBe(''); + expect(component.currentPrescription.drugName).toBe(''); + expect(component.isStockAvalable).toBe(''); + }); + + it('alerts when drug is already prescribed', () => { + component.drugPrescriptionForm = drugForm([{ drugID: 100 }]); + component.tempDrugName = null; + component.currentPrescription.formName = 'Tablet'; + component.selectMedicineObject(option()); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.medicinePrescribe, + 'info' + ); + expect(component.currentPrescription.drugID).toBeNull(); + }); + }); + + describe('add / clear', () => { + beforeEach(() => create('new', drugForm(), masterData())); + + it('submitForUpload inserts medicine and resets all fields', () => { + Object.assign(component.currentPrescription, { + drugID: 100, + drugName: 'Paracetamol', + drugStrength: '500', + drugUnit: 'mg', + formName: 'Tablet', + formID: 1, + dose: '1 tab', + duration: 2, + unit: 'Days', + }); + component.tempform = 'x'; + component.submitForUpload(); + const arr = drugsArray(); + expect(arr.length).toBe(1); + expect(arr.at(0).value.drugName).toBe('Paracetamol'); + expect(arr.at(0).value.drugStrength).toBe('500mg'); + expect(arr.at(0).value.createdBy).toBe('drUser'); + expect(arr.at(0).value.id).toBeNull(); + expect(component.tempform).toBeNull(); + expect(component.currentPrescription.formName).toBeNull(); + expect(component.currentPrescription.drugID).toBeNull(); + expect(component.currentPrescription.formID).toBeNull(); + expect(component.currentPrescription.dose).toBeNull(); + expect(component.isStockAvalable).toBe(''); + }); + + it('renders added medicine in the list', () => { + component.drugPrescriptionForm = drugForm([ + { + id: 1, + drugName: 'Paracetamol', + drugStrength: '500mg', + isEDL: false, + createdBy: 'x', + formName: 'Tablet', + dose: '', + frequency: '', + durationView: '', + qtyPrescribed: '', + route: '', + instructions: '', + }, + ]); + fixture.detectChanges(); + const legend: HTMLElement = fixture.nativeElement.querySelector('legend'); + expect(legend.textContent).toContain('Paracetamol'); + expect(legend.textContent).toContain(LANGUAGE_EN.nonEDLMedicine); + }); + }); + + describe('delete', () => { + beforeEach(() => + create( + 'new', + drugForm([ + { id: null, drugID: 1 }, + { id: 9, drugID: 2 }, + ]), + masterData() + ) + ); + + it('removes UI-only medicine after confirm', () => { + component.deleteMedicine(0); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.confirmDelete + ); + expect(drugsArray().length).toBe(1); + expect(drugsArray().at(0).value.id).toBe(9); + expect(doctor.deleteMedicine).not.toHaveBeenCalled(); + }); + + it('deletes saved medicine through backend', () => { + doctor.deleteMedicine.and.returnValue(of({ statusCode: 200 })); + component.deleteMedicine(1, 9 as any); + expect(doctor.deleteMedicine).toHaveBeenCalledWith(9); + expect(drugsArray().length).toBe(1); + expect(drugsArray().at(0).value.drugID).toBe(1); + }); + + it('keeps medicine when backend delete fails', () => { + doctor.deleteMedicine.and.returnValue(of({ statusCode: 500 })); + component.deleteMedicineBackend(1, 9); + expect(drugsArray().length).toBe(2); + }); + + it('does nothing when delete not confirmed', () => { + confirm.confirm.and.returnValue(of(false)); + component.deleteMedicine(0); + expect(drugsArray().length).toBe(2); + }); + }); + + describe('editMedicine', () => { + const row = (over: any = {}) => ({ + id: null, + drugID: 100, + drugName: 'Paracetamol', + formName: 'Tablet', + quantity: 20, + sctCode: 'S1', + sctTerm: 'T1', + drugStrength: '500', + drugUnit: 'mg', + dose: '1 tab', + frequency: 'OD', + duration: 3, + unit: 'Days', + qtyPrescribed: 6, + route: 'Oral', + instructions: 'after food', + ...over, + }); + + it('loads row into current prescription and removes it from UI', () => { + const data = masterData(); + Object.assign(data.itemMaster[0], { isEDL: true }); + create('new', drugForm([row()]), data); + component.editMedicine(0, null); + const cp = component.currentPrescription; + expect(cp.formID).toBe(1); + expect(cp.drugID).toBe(100); + expect(cp.dose).toBe('1 tab'); + expect(cp.frequency).toBe('OD'); + expect(cp.duration).toBe(3); + expect(cp.unit).toBe('Days'); + expect(cp.qtyPrescribed).toBe(6); + expect(cp.route).toBe('Oral'); + expect(cp.instructions).toBe('after food'); + expect(cp.isEDL).toBeTrue(); + expect(component.tempDrugName.itemName).toBe('Paracetamol'); + expect(component.isStockAvalable).toBe('primary'); + expect(drugsArray().length).toBe(0); + }); + + it('deletes saved row through backend while editing', () => { + doctor.deleteMedicine.and.returnValue(of({ statusCode: 200 })); + create('new', drugForm([row({ id: 4 })]), masterData()); + component.editMedicine(0, 4); + expect(doctor.deleteMedicine).toHaveBeenCalledWith(4); + expect(drugsArray().length).toBe(0); + }); + }); + + describe('setMedicineObject', () => { + beforeEach(() => create('new', drugForm(), masterData())); + + it('copies a complete option and flags available stock', () => { + component.setMedicineObject({ + id: 1, + itemName: 'A', + itemID: 2, + quantityInHand: 3, + sctCode: 'S', + sctTerm: 'T', + strength: '5', + unitOfMeasurement: 'mg', + isEDL: true, + }); + expect(component.currentPrescription.drugName).toBe('A'); + expect(component.currentPrescription.drugID).toBe(2); + expect(component.isStockAvalable).toBe('primary'); + }); + + it('does not copy incomplete option and flags missing stock', () => { + component.setMedicineObject({ + itemName: 'B', + quantityInHand: 0, + isEDL: false, + }); + expect(component.currentPrescription.drugName).toBeNull(); + expect(component.isStockAvalable).toBe('warn'); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-significiant-findings/previous-significiant-findings.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-significiant-findings/previous-significiant-findings.component.spec.ts index cf749387..e96c9652 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-significiant-findings/previous-significiant-findings.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-significiant-findings/previous-significiant-findings.component.spec.ts @@ -20,27 +20,109 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { DoctorService } from '../../../shared/services'; import { PreviousSignificiantFindingsComponent } from './previous-significiant-findings.component'; describe('PreviousSignificiantFindingsComponent', () => { let component: PreviousSignificiantFindingsComponent; let fixture: ComponentFixture; + let doctor: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PreviousSignificiantFindingsComponent], - }).compileComponents(); - })); + const findings = () => [ + { significantFindings: 'Hypertension', captureDate: '2024-01-01' }, + { significantFindings: 'Diabetes', captureDate: '2024-02-01' }, + { significantFindings: 'Asthma', captureDate: '2024-03-01' }, + ]; - beforeEach(() => { + const create = (resp: any) => { + doctor.getPreviousSignificiantFindings.and.returnValue(of(resp)); fixture = TestBed.createComponent(PreviousSignificiantFindingsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PreviousSignificiantFindingsComponent], + providers: [ + ...commonTestProviders({ session: { beneficiaryRegID: 77 } }), + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + + afterEach(() => fixture.destroy()); + + it('loads findings for the beneficiary', () => { + create({ statusCode: 200, data: { findings: findings() } }); + expect(doctor.getPreviousSignificiantFindings).toHaveBeenCalledWith({ + beneficiaryRegID: 77, + }); + expect(component.previousSignificiantFindingsList.length).toBe(3); + expect(component.filteredPreviousSignificiantFindingsList).toBe( + component.previousSignificiantFindingsList + ); + expect(component.dataSource.data.length).toBe(3); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('keeps list empty when response has no findings', () => { + create({ statusCode: 200, data: null }); + expect(component.previousSignificiantFindingsList).toEqual([]); + expect(component.dataSource.data).toEqual([]); + }); + + it('keeps list empty on non-200', () => { + create({ statusCode: 500, data: { findings: findings() } }); + expect(component.previousSignificiantFindingsList).toEqual([]); + }); + + it('filters findings by any field, case-insensitive', () => { + create({ statusCode: 200, data: { findings: findings() } }); + component.filterPreviousSignificiantFindingsList('DIAB'); + expect(component.filteredPreviousSignificiantFindingsList).toEqual([ + findings()[1], + ] as any); + component.filterPreviousSignificiantFindingsList('2024-03'); + expect(component.filteredPreviousSignificiantFindingsList).toEqual([ + findings()[2], + ] as any); + // current behaviour: search empties the table data source + expect(component.dataSource.data).toEqual([]); + }); + + it('restores full list for empty search term', () => { + create({ statusCode: 200, data: { findings: findings() } }); + component.filterPreviousSignificiantFindingsList('asthma'); + component.filterPreviousSignificiantFindingsList(''); + expect(component.filteredPreviousSignificiantFindingsList.length).toBe(3); + }); + + it('pageChanged slices filtered list', () => { + create({ statusCode: 200, data: { findings: findings() } }); + component.pageChanged({ page: 2, itemsPerPage: 2 }); + expect(component.pagedList).toEqual([findings()[2]] as any); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('unsubscribes on destroy', () => { + create({ statusCode: 200, data: null }); + const sub = component.previousSignificantFindingsSubs; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + component.previousSignificantFindingsSubs = null; + expect(() => component.ngOnDestroy()).not.toThrow(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-visit-details/previous-visit-details.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-visit-details/previous-visit-details.component.spec.ts index e58a2700..c199a89f 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-visit-details/previous-visit-details.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/previous-visit-details/previous-visit-details.component.spec.ts @@ -20,27 +20,178 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DoctorService } from '../../../shared/services'; +import { CameraService } from '../../../../core/services/camera.service'; import { PreviousVisitDetailsComponent } from './previous-visit-details.component'; describe('PreviousVisitDetailsComponent', () => { let component: PreviousVisitDetailsComponent; let fixture: ComponentFixture; + let doctor: any; + let camera: any; + let session: any; + let router: Router; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PreviousVisitDetailsComponent], - }).compileComponents(); - })); + const graphData = () => ({ + bpList: [ + { date: '2024-01-01', avgSysBP: 120, avgDysBP: 80 }, + { date: '2024-03-01', avgSysBP: 140, avgDysBP: 90 }, + { date: '2024-02-01', avgSysBP: 0, avgDysBP: 85 }, + ], + weightList: [ + { date: '2024-01-01', weight: 60 }, + { date: '2024-02-01', weight: 61 }, + { date: '2024-03-01' }, + ], + bgList: [ + { date: '2024-01-01', bg_2hr_pp: 150, bg_fasting: 95, bg_random: 120 }, + { date: '2024-02-01', bg_2hr_pp: 160, bg_fasting: 99, bg_random: 130 }, + { date: '2024-03-01', bg_fasting: 99 }, + ], + }); - beforeEach(() => { + const create = (resp: any) => { + doctor.getCaseRecordAndReferDetails.and.returnValue(of(resp)); fixture = TestBed.createComponent(PreviousVisitDetailsComponent); component = fixture.componentInstance; fixture.detectChanges(); + }; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + camera = autoSpy(CameraService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PreviousVisitDetailsComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: 1, + visitID: 2, + visitCategory: 'NCD care', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: CameraService, useValue: camera }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + }); + + afterEach(() => fixture.destroy()); + + it('loads graph data from case record and plots all graphs newest first', () => { + create({ statusCode: 200, data: { GraphData: graphData() } }); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 1, + 2, + 'NCD care' + ); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + expect(component.bpChartLabels).toEqual(['2024-03-01', '2024-01-01']); + expect(component.bpChartData).toEqual([ + { data: [140, 120], label: 'Systolic BP' }, + { data: [90, 80], label: 'Diastolic BP' }, + ]); + expect(component.weightChartData).toEqual([ + { data: [61, 60], label: 'Weight' }, + ]); + expect(component.weightChartLabels).toEqual(['2024-02-01', '2024-01-01']); + expect(component.bgChartLabels).toEqual(['2024-02-01', '2024-01-01']); + expect(component.bgChartData).toEqual([ + { data: [160, 150], label: '2-Hr Post Prandial' }, + { data: [99, 95], label: 'Fasting' }, + { data: [130, 120], label: 'Random' }, + ]); + expect(fixture.nativeElement.querySelectorAll('.no_records').length).toBe( + 0 + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('shows no-record placeholders when there is no graph data', () => { + create({ statusCode: 200, data: {} }); + expect(component.bpChartData).toEqual([]); + expect(component.weightChartData).toEqual([]); + expect(component.bgChartData).toEqual([]); + expect(fixture.nativeElement.querySelectorAll('.no_records').length).toBe( + 3 + ); + }); + + it('plots nothing for empty lists', () => { + create({ statusCode: 200, data: {} }); + component.plotGraphs({ bpList: [], weightList: [], bgList: [] }); + expect(component.bpChartData).toEqual([]); + expect(component.weightChartData).toEqual([]); + expect(component.bgChartData).toEqual([]); + }); + + describe('after load', () => { + beforeEach(() => create({ statusCode: 200, data: {} })); + + it('calculateBMI returns BMI or 0 without vitals', () => { + expect(component.calculateBMI()).toBe(0); + component.currentVitals = { weight_Kg: 81, height_cm: 180 }; + expect(component.calculateBMI()).toBe(25); + }); + + it('getCaseSheetPrintData stores visit and navigates to print', () => { + component.getCaseSheetPrintData({ + createdDate: '2024-05-06T07:08:09.000Z', + visitCategory: 'ANC', + beneficiaryRegID: 3, + benVisitID: 4, + }); + expect(component.visitDateTime).toBe('2024-05-06T07:08:09.000Z'); + expect(session.setItem).toHaveBeenCalledWith('caseSheetBenFlowID', ''); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategory', + 'ANC' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetBeneficiaryRegID', + 3 + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetVisitID', 4); + expect(router.navigate).toHaveBeenCalledWith(['/nurse-doctor/print']); + }); + + ['bw', 'bp', 'bg'].forEach(type => { + it(`chartClicked('${type}') opens a graph with 7 properties`, () => { + component.chartClicked(type); + expect(camera.ViewGraph).toHaveBeenCalledTimes(1); + const arg = camera.ViewGraph.calls.mostRecent().args[0]; + expect(arg.type).toBe(type); + expect(Object.keys(arg).length).toBe(7); + expect(arg.chartType).toBe('line'); + expect(arg.lineChartOptions).toBe(component.lineChartOptions); + }); + }); + + it('chartClicked ignores unknown types', () => { + component.chartClicked('xx'); + expect(camera.ViewGraph).not.toHaveBeenCalled(); + }); + + it('graph callers skip incomplete objects', () => { + component.callBodyWeightGraph({}); + component.callBloodPressureGraph({ a: 1 }); + component.callBloodGlucoseGraph({ a: 1, b: 2 }); + expect(camera.ViewGraph).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/test-and-radiology.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/test-and-radiology.component.spec.ts index 4400393a..a6c11460 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/test-and-radiology.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/test-and-radiology.component.spec.ts @@ -20,27 +20,318 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; +import { LabService } from 'src/app/app-modules/lab/shared/services'; +import { ViewRadiologyUploadedFilesComponent } from 'src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component'; +import { DoctorService } from '../../../shared/services'; +import { IdrsscoreService } from '../../../shared/services/idrsscore.service'; +import { TestInVitalsService } from '../../../shared/services/test-in-vitals.service'; +import { ViewTestReportComponent } from './view-test-report/view-test-report.component'; import { TestAndRadiologyComponent } from './test-and-radiology.component'; describe('TestAndRadiologyComponent', () => { let component: TestAndRadiologyComponent; let fixture: ComponentFixture; + let doctor: any; + let lab: any; + let idrs: any; + let dialog: any; + let vitals: TestInVitalsService; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TestAndRadiologyComponent], - }).compileComponents(); - })); + const labReport = (extra: any[] = []) => [ + { + procedureID: 1, + procedureName: environment.RBSTest, + procedureType: 'Laboratory', + componentList: [ + { stripsNotAvailable: true }, + { stripsNotAvailable: false }, + ], + }, + { + procedureID: 2, + procedureName: 'Haemoglobin', + procedureType: 'Laboratory', + componentList: [], + }, + { procedureID: 3, procedureName: 'X-Ray', procedureType: 'Radiology' }, + ...extra, + ]; + const reportResponse = (list = labReport()) => ({ + statusCode: 200, + data: { + LabReport: list, + ArchivedVisitcodeForLabResult: [{ visitCode: 9 }], + }, + }); - beforeEach(() => { + const setup = async (visitCategory = 'NCD screening') => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [TestAndRadiologyComponent], + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: 'B1', visitID: 'V1', visitCategory }, + }), + TestInVitalsService, + { provide: DoctorService, useValue: doctor }, + { provide: LabService, useValue: lab }, + { provide: IdrsscoreService, useValue: idrs }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(TestAndRadiologyComponent, '') + .compileComponents(); fixture = TestBed.createComponent(TestAndRadiologyComponent); component = fixture.componentInstance; - fixture.detectChanges(); + dialog = TestBed.inject(MatDialog); + vitals = TestBed.inject(TestInVitalsService); + }; + + beforeEach(() => { + doctor = autoSpy(DoctorService); + lab = autoSpy(LabService); + idrs = autoSpy(IdrsscoreService); + doctor.getCaseRecordAndReferDetails.and.returnValue(of(reportResponse())); + spyOn(console, 'log'); + }); + + afterEach(() => component?.ngOnDestroy()); + + describe('ngOnInit', () => { + it('loads lab and radiology results and flags missing RBS strips (NCD screening)', async () => { + await setup(); + component.ngOnInit(); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'B1', + 'V1', + 'NCD screening' + ); + expect(component.labResults.length).toBe(2); + expect(component.radiologyResults.map((r: any) => r.procedureID)).toEqual( + [3] + ); + expect(component.archivedResults).toEqual([{ visitCode: 9 }]); + expect(component.filteredLabResults.data.length).toBe(2); + expect(component.currentLabPagedList.length).toBe(2); + expect(idrs.setReferralSuggested).toHaveBeenCalledTimes(1); + }); + + it('does not check strips outside NCD screening', async () => { + await setup('General OPD'); + component.ngOnInit(); + expect(idrs.setReferralSuggested).not.toHaveBeenCalled(); + expect(component.labResults.length).toBe(2); + }); + + it('ignores failed responses', async () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 500 }) + ); + await setup(); + component.ngOnInit(); + expect(component.labResults).toEqual([]); + }); + + it('prepends vitals RBS result when emitted', async () => { + await setup(); + component.ngOnInit(); + vitals.setVitalsRBSValueInReports({ + visitCode: 1, + rbsTestResult: 180, + rbsTestRemarks: 'fasting', + createdDate: 'd1', + }); + expect(component.vitalsRBSResp.componentList[0].testResultValue).toBe( + 180 + ); + expect(component.labResults[0]).toBe(component.vitalsRBSResp); + expect(component.labResults.length).toBe(3); + expect(component.labResults[0].componentList[0].remarks).toBe('fasting'); + }); + + it('ignores vitals emission without RBS result', async () => { + await setup(); + component.ngOnInit(); + vitals.setVitalsRBSValueInReports({ visitCode: 1 }); + expect(component.vitalsRBSResp).toBeNull(); + }); + }); + + it('ngDoCheck refreshes language', async () => { + await setup(); + component.ngDoCheck(); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + describe('checkRBSResultInVitalsUpdate', () => { + beforeEach(async () => { + await setup(); + component.ngOnInit(); + }); + + it('replaces existing vitals RBS entry with the new value', () => { + component.labResults = [ + { procedureName: 'RBS Test', procedureID: null }, + { procedureName: 'Other', procedureID: 5 }, + ]; + vitals.setVitalsRBSValueInReportsInUpdate({ + rbsTestResult: 99, + rbsTestRemarks: 'r', + createdDate: 'd2', + }); + expect(component.labResults.length).toBe(2); + expect(component.labResults[0].componentList[0].testResultValue).toBe(99); + expect(component.labResults[1].procedureID).toBe(5); + expect(component.currentLabPagedList.length).toBe(2); + }); + + it('removes vitals RBS entry when no result', () => { + component.labResults = [ + { procedureName: 'RBS Test', procedureID: null }, + { procedureName: 'Other', procedureID: 5 }, + ]; + component.checkRBSResultInVitalsUpdate({}); + expect(component.labResults).toEqual([ + { procedureName: 'Other', procedureID: 5 }, + ]); + expect(component.filteredLabResults.data.length).toBe(1); + }); + }); + + describe('filtering and paging', () => { + beforeEach(async () => { + await setup(); + component.ngOnInit(); + }); + + it('filterProcedures filters by name and resets page', () => { + component.currentLabActivePage = 3; + component.filterProcedures('haem'); + expect( + component.filteredLabResults.data.map((r: any) => r.procedureID) + ).toEqual([2]); + expect(component.currentLabActivePage).toBe(1); + expect(component.currentLabPagedList.length).toBe(1); + component.filterProcedures(); + expect(component.filteredLabResults.data.length).toBe(2); + }); + + it('currentLabPageChanged slices the page', () => { + component.filteredLabResults.data = [1, 2, 3, 4, 5, 6, 7]; + component.currentLabPageChanged({ page: 2, itemsPerPage: 5 }); + expect(component.currentLabPagedList).toEqual([6, 7]); + }); + }); + + describe('archived results', () => { + beforeEach(async () => { + await setup(); + }); + + it('loads archived lab/radiology reports', () => { + doctor.getArchivedReports.and.returnValue( + of({ + statusCode: 200, + data: [ + { procedureName: 'Sugar', procedureType: 'Laboratory' }, + { procedureName: 'Urine', procedureType: 'Laboratory' }, + { procedureName: 'CT', procedureType: 'Radiology' }, + ], + }) + ); + component.showArchivedTestResult({ visitCode: 'VC', date: 'D' }); + expect(doctor.getArchivedReports).toHaveBeenCalledWith({ + beneficiaryRegID: 'B1', + visitCode: 'VC', + }); + expect(component.archivedLabResults.length).toBe(2); + expect(component.archivedRadiologyResults.length).toBe(1); + expect(component.previousLabPagedList.length).toBe(2); + expect(component.enableArchiveView).toBeTrue(); + expect(component.visitedDate).toBe('D'); + expect(component.visitCode).toBe('VC'); + + component.filterArchivedProcedures('uri'); + expect(component.filteredArchivedLabResults).toEqual([ + { procedureName: 'Urine', procedureType: 'Laboratory' }, + ]); + expect(component.previousLabActivePage).toBe(1); + component.filterArchivedProcedures(''); + expect(component.filteredArchivedLabResults.length).toBe(2); + + component.resetArchived(); + expect(component.archivedLabResults).toEqual([]); + expect(component.filteredArchivedLabResults).toEqual([]); + expect(component.archivedRadiologyResults).toEqual([]); + expect(component.visitCode).toBeNull(); + expect(component.visitedDate).toBeNull(); + expect(component.enableArchiveView).toBeFalse(); + expect(component.previousLabPagedList).toEqual([]); + }); + + it('ignores failed archived fetch', () => { + doctor.getArchivedReports.and.returnValue(of({ statusCode: 500 })); + component.showArchivedTestResult({ visitCode: 'VC' }); + expect(component.enableArchiveView).toBeFalse(); + }); + + it('opens radiology report dialog', () => { + const report = { id: 1 }; + component.showArchivedRadiologyTestResult(report); + expect(dialog.open).toHaveBeenCalledWith( + ViewTestReportComponent, + jasmine.objectContaining({ data: report, panelClass: 'dialog-width' }) + ); + }); + }); + + describe('showTestResult', () => { + beforeEach(async () => { + await setup(); + }); + + it('opens uploaded-files dialog and downloads the chosen file', () => { + dialog.open.and.returnValue(createDialogRefMock({ fileName: 'f.pdf' })); + lab.viewFileContent.and.returnValue(of(new Blob(['x']))); + spyOn(window.URL, 'createObjectURL').and.returnValue('blob:x'); + const click = spyOn(HTMLAnchorElement.prototype, 'click'); + component.showTestResult([1, 2]); + expect(dialog.open).toHaveBeenCalledWith( + ViewRadiologyUploadedFilesComponent, + jasmine.objectContaining({ + width: '40%', + data: jasmine.objectContaining({ filesDetails: [1, 2] }), + }) + ); + expect(lab.viewFileContent).toHaveBeenCalledWith({ fileName: 'f.pdf' }); + expect(click).toHaveBeenCalled(); + }); + + it('does nothing when dialog is dismissed', () => { + dialog.open.and.returnValue(createDialogRefMock(undefined)); + component.showTestResult([]); + expect(lab.viewFileContent).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes test results', async () => { + await setup(); + const s = jasmine.createSpyObj('s', ['unsubscribe']); + component.testResultsSubscription = s; + component.ngOnDestroy(); + expect(s.unsubscribe).toHaveBeenCalled(); + component.testResultsSubscription = null; }); }); diff --git a/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/view-test-report/view-test-report.component.spec.ts b/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/view-test-report/view-test-report.component.spec.ts index 397ee3ab..3b4e1b24 100644 --- a/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/view-test-report/view-test-report.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-record/general-case-record/test-and-radiology/view-test-report/view-test-report.component.spec.ts @@ -20,27 +20,52 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { ViewTestReportComponent } from './view-test-report.component'; describe('ViewTestReportComponent', () => { let component: ViewTestReportComponent; let fixture: ComponentFixture; + const report = [ + { procedureName: 'CBC', componentName: 'Hb', testResultValue: '12' }, + { procedureName: 'RBS', componentName: 'Glucose', testResultValue: '90' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ViewTestReportComponent], + providers: [...commonTestProviders({ dialogData: report })], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ViewTestReportComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('loads dialog data into the table and sets language', () => { + expect(component.data).toBe(report); + expect(component.dataSource.data).toEqual(report); + expect(component.displayedColumns).toEqual([ + 'procedureName', + 'componentName', + 'testresult', + 'unit', + ]); + expect(component.current_language_set).toEqual(LANGUAGE_EN); + }); + + it('exposes the dialog ref for closing', () => { + component.matDialogRef.close(); + expect(TestBed.inject(MatDialogRef).close).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-case-sheet.component.spec.ts index a64af34c..1f82992e 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-case-sheet.component.spec.ts @@ -20,27 +20,271 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Location } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { PrintPageSelectComponent } from '../../print-page-select/print-page-select.component'; import { CancerCaseSheetComponent } from './cancer-case-sheet.component'; describe('CancerCaseSheetComponent', () => { let component: CancerCaseSheetComponent; let fixture: ComponentFixture; + let doctor: any; + let confirm: any; + let dialog: any; + let location: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async (session: Record = {}, params: any = {}) => { + doctor = autoSpy(DoctorService); + location = { back: jasmine.createSpy('back') }; + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerCaseSheetComponent], + providers: [ + ...commonTestProviders({ session }), + { provide: DoctorService, useValue: doctor }, + { provide: Location, useValue: location }, + { provide: ActivatedRoute, useValue: { snapshot: { params } } }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + spyOn(console, 'log'); + }; + + describe('ngOnInit', () => { + it('current page: builds request, visit ids, oncologist role and loads TM data', async () => { + await setup( + { + caseSheetVisitCategory: 'Cancer Screening', + caseSheetBenFlowID: 'f', + caseSheetVisitID: 'v', + caseSheetBeneficiaryRegID: 'r', + visitCode: 'vc', + currentRole: 'Oncologist', + }, + { printablePage: 'current' } + ); + component.serviceType = 'TM'; + doctor.getTMCasesheetData.and.returnValue( + of({ statusCode: 200, data: { x: 1 } }) + ); + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(doctor.getTMCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'Cancer Screening', + benFlowID: 'f', + benVisitID: 'v', + beneficiaryRegID: 'r', + visitCode: 'vc', + }); + expect(component.getCaseSheetDataVisit).toEqual({ + benVisitID: 'v', + benRegID: 'r', + benFlowID: 'f', + }); + expect(component.visitCategory).toBe('Cancer Screening'); + expect(component.oncologistRemarks).toBeTrue(); + expect(component.hideBack).toBeFalse(); + expect(component.caseSheetData).toEqual({ x: 1 }); + }); + + it('previous page (default): loads MMU data and hides back', async () => { + await setup({ + previousCaseSheetVisitCategory: 'Cancer Screening', + previousCaseSheetBenFlowID: 'pf', + previousCaseSheetVisitID: 'pv', + previousCaseSheetBeneficiaryRegID: 'pr', + previousCaseSheetVisitCode: 'pc', + currentRole: 'Doctor', + }); + component.serviceType = 'MMU'; + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: { y: 2 } }) + ); + component.ngOnInit(); + expect(component.hideBack).toBeTrue(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'Cancer Screening', + benFlowID: 'pf', + beneficiaryRegID: 'pr', + visitCode: 'pc', + }); + expect(component.getCaseSheetDataVisit).toEqual({ + benVisitID: 'pv', + benRegID: 'pr', + benFlowID: 'pf', + }); + expect(component.oncologistRemarks).toBeUndefined(); + expect(component.caseSheetData).toEqual({ y: 2 }); + }); + + it('ignores failed casesheet responses and unknown service types', async () => { + await setup({}, { printablePage: 'nope' }); + component.ngOnInit(); + component.serviceType = 'TM'; + doctor.getTMCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.getCaseSheetData({}); + component.serviceType = 'MMU'; + doctor.getMMUCasesheetData.and.returnValue(of(null)); + component.getCaseSheetData({}); + expect(component.caseSheetData).toBeUndefined(); + }); + }); + + describe('oncologist remarks', () => { + beforeEach(async () => { + await setup(); + component.fetchLanguageResponse(); + component.getCaseSheetDataVisit = { benVisitID: 'v', benRegID: 'r' }; + }); + + it('edits existing remarks, trims after last dot and saves', () => { + component.caseSheetDiagnosisData = { + provisionalDiagnosisOncologist: 'old', + }; + confirm.editRemarks.and.returnValue(of('new remark.xyz')); + doctor.postOncologistRemarksforCancerCaseSheet.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + component.caseSheetData = { doctorData: { diagnosis: { a: 1 } } }; + component.getOncologistRemarks(); + expect(confirm.editRemarks).toHaveBeenCalledWith( + 'Oncologist Observation', + 'old' + ); + expect( + component.caseSheetDiagnosisData.provisionalDiagnosisOncologist + ).toBe('new remark'); + expect( + doctor.postOncologistRemarksforCancerCaseSheet + ).toHaveBeenCalledWith('new remark', 'v', 'r'); + expect(component.caseSheetData.doctorData.diagnosis).toEqual({ + a: 1, + provisionalDiagnosisOncologist: 'new remark', + }); + expect(confirm.alert).toHaveBeenCalledWith('saved', 'success'); + }); + + it('creates diagnosis data when absent; success without doctorData', () => { + confirm.editRemarks.and.returnValue(of('r1.')); + doctor.postOncologistRemarksforCancerCaseSheet.and.returnValue( + of({ statusCode: 200, data: { response: 'ok' } }) + ); + component.getOncologistRemarks(); + expect(confirm.editRemarks).toHaveBeenCalledWith( + 'Oncologist Observation', + undefined + ); + expect(component.caseSheetDiagnosisData).toEqual({ + provisionalDiagnosisOncologist: 'r1', + }); + expect(component.caseSheetData).toBeUndefined(); + expect(confirm.alert).toHaveBeenCalledWith('ok', 'success'); + }); + + it('does nothing when edit is cancelled', () => { + confirm.editRemarks.and.returnValue(of(null)); + component.getOncologistRemarks(); + expect( + doctor.postOncologistRemarksforCancerCaseSheet + ).not.toHaveBeenCalled(); + }); + + it('alerts error on 500/5000 and nothing on other codes', () => { + doctor.postOncologistRemarksforCancerCaseSheet.and.returnValue( + of({ statusCode: 5000, errorMessage: 'fail' }) + ); + component.saveOncologistRemarks('x'); + expect(confirm.alert).toHaveBeenCalledWith('fail', 'error'); + doctor.postOncologistRemarksforCancerCaseSheet.and.returnValue( + of({ statusCode: 500, errorMessage: 'fail2' }) + ); + component.saveOncologistRemarks('x'); + expect(confirm.alert).toHaveBeenCalledWith('fail2', 'error'); + confirm.alert.calls.reset(); + doctor.postOncologistRemarksforCancerCaseSheet.and.returnValue( + of({ statusCode: 400 }) + ); + component.saveOncologistRemarks('x'); + expect(confirm.alert).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('print page selection and helpers', () => { + beforeEach(async () => setup()); + + it('applies dialog selection', () => { + dialog.open.and.returnValue( + createDialogRefMock({ + caseSheetExamination: false, + caseSheetHistory: false, + caseSheetCovidVaccinationDetails: false, + }) + ); + component.visitCategory = 'Cancer Screening'; + component.selectPrintPage(); + expect(dialog.open).toHaveBeenCalledWith(PrintPageSelectComponent, { + width: '420px', + disableClose: false, + data: { + printPagePreviewSelect: component.printPagePreviewSelect, + visitCategory: 'Cancer Screening', + }, + }); + expect(component.printPagePreviewSelect).toEqual({ + caseSheetHistory: false, + caseSheetExamination: false, + caseSheetCovidVaccinationDetails: false, + }); + }); + + it('keeps selection when dismissed', () => { + component.selectPrintPage(); + expect(component.printPagePreviewSelect.caseSheetHistory).toBeTrue(); + }); + + it('printPage, goToTop, goBack use browser APIs', () => { + const print = spyOn(window, 'print'); + const scroll = spyOn(window, 'scrollTo'); + component.printPage(); + component.goToTop(); + component.goBack(); + expect(print).toHaveBeenCalled(); + expect(scroll).toHaveBeenCalledWith(0, 0); + expect(location.back).toHaveBeenCalled(); + }); + + it('ngOnDestroy unsubscribes when subscribed', () => { + const sub = { unsubscribe: jasmine.createSpy('u') }; + component.caseSheetSubs = sub; + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + component.caseSheetSubs = undefined; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('language helpers set current language', () => { + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-doctor-diagnosis-case-sheet/cancer-doctor-diagnosis-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-doctor-diagnosis-case-sheet/cancer-doctor-diagnosis-case-sheet.component.spec.ts index 66199127..486a9a26 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-doctor-diagnosis-case-sheet/cancer-doctor-diagnosis-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-doctor-diagnosis-case-sheet/cancer-doctor-diagnosis-case-sheet.component.spec.ts @@ -20,27 +20,196 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MasterdataService } from '../../../shared/services'; import { CancerDoctorDiagnosisCaseSheetComponent } from './cancer-doctor-diagnosis-case-sheet.component'; describe('CancerDoctorDiagnosisCaseSheetComponent', () => { let component: CancerDoctorDiagnosisCaseSheetComponent; let fixture: ComponentFixture; + let master: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async (session: Record = {}) => { + master = autoSpy(MasterdataService); + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 200, data: null }) + ); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerDoctorDiagnosisCaseSheetComponent], + providers: [ + ...commonTestProviders({ + session: { caseSheetBeneficiaryRegID: '42', ...session }, + }), + { provide: MasterdataService, useValue: master }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerDoctorDiagnosisCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + spyOn(console, 'log'); + }; + + describe('ngOnInit', () => { + it('sets date and language, doctor sign off by default', async () => { + await setup(); + component.ngOnInit(); + const t = new Date(); + expect(component.date).toBe( + t.getDate() + '/' + (t.getMonth() + 1) + '/' + t.getFullYear() + ); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.enableDoctorSign).toBeFalse(); + }); + + it('enables doctor sign for TM flag', async () => { + await setup({ caseSheetTMFlag: 'true' }); + component.ngOnInit(); + expect(component.enableDoctorSign).toBeTrue(); + }); + + it('enables doctor sign for specialist flag 200', async () => { + await setup({ specialistFlag: '200' }); + component.ngOnInit(); + expect(component.enableDoctorSign).toBeTrue(); + }); + }); + + describe('ngOnChanges', () => { + beforeEach(async () => setup()); + + it('does nothing without data', () => { + component.ngOnChanges(); + expect(component.beneficiaryDetails).toBeUndefined(); + }); + + it('formats dates and maps vitals, diagnosis, service point', () => { + component.caseSheetData = { + BeneficiaryData: { + serviceDate: '2024-01-02T03:04:05', + consultationDate: '2024-11-12T13:14:15', + ageVal: 5, + }, + nurseData: { + benVisitDetail: { serviceProviderName: 'SP' }, + currentVitals: { bp: 1 }, + }, + doctorData: { diagnosis: { d: 1 } }, + }; + component.ngOnChanges(); + expect(component.servicePointName).toBe('SP'); + expect(component.beneficiaryDetails.serviceDate).toBe( + '02/01/2024 03:04:05' + ); + expect(component.beneficiaryDetails.consultationDate).toBe( + '12/11/2024 13:14:15' + ); + expect(component.currentVitals).toEqual({ bp: 1 }); + expect(component.caseSheetDiagnosisData).toEqual({ d: 1 }); + expect(master.getVaccinationTypeAndDoseMaster).not.toHaveBeenCalled(); + }); + + it('skips optional sections and fetches vaccination for adults', () => { + component.caseSheetData = { + BeneficiaryData: { ageVal: 30 }, + nurseData: { benVisitDetail: {} }, + }; + component.ngOnChanges(); + expect(component.currentVitals).toBeUndefined(); + expect(component.caseSheetDiagnosisData).toBeUndefined(); + expect(master.getVaccinationTypeAndDoseMaster).toHaveBeenCalled(); + }); + }); + + describe('covid vaccination', () => { + beforeEach(async () => { + await setup(); + component.beneficiaryDetails = { ageVal: 20 }; + }); + + it('loads masters and previous vaccination with filtered types', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ + statusCode: 200, + data: { + doseType: [{ covidDoseTypeID: 1 }, { covidDoseTypeID: 2 }], + vaccineType: [{ covidVaccineTypeID: 7 }], + }, + }) + ); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ + statusCode: 200, + data: { covidVSID: 1, doseTypeID: 1, covidVaccineTypeID: 7 }, + }) + ); + component.getVaccinationTypeAndDoseMaster(); + expect(master.getPreviousCovidVaccinationDetails).toHaveBeenCalledWith( + '42' + ); + expect(component.covidVaccineDetails.doseTypeID).toEqual([ + { covidDoseTypeID: 1 }, + ]); + expect(component.covidVaccineDetails.covidVaccineTypeID).toEqual([ + { covidVaccineTypeID: 7 }, + ]); + }); + + it('stores data without type ids; ignores missing covidVSID and non-200', () => { + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: { covidVSID: 1 } }) + ); + component.getPreviousCovidVaccinationDetails([], []); + expect(component.covidVaccineDetails).toEqual({ covidVSID: 1 }); + component.covidVaccineDetails = undefined; + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getPreviousCovidVaccinationDetails([], []); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPreviousCovidVaccinationDetails([], []); + expect(component.covidVaccineDetails).toBeUndefined(); + + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 500 }) + ); + master.getPreviousCovidVaccinationDetails.calls.reset(); + component.getVaccinationTypeAndDoseMaster(); + expect(master.getPreviousCovidVaccinationDetails).not.toHaveBeenCalled(); + }); + + it('logs errors from both calls', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + throwingObs({ errorMessage: 'm' }) + ); + master.getPreviousCovidVaccinationDetails.and.returnValue( + throwingObs({ errorMessage: 'p' }) + ); + component.getVaccinationTypeAndDoseMaster(); + component.getPreviousCovidVaccinationDetails([], []); + expect(console.log).toHaveBeenCalledWith('error', 'm'); + expect(console.log).toHaveBeenCalledWith('error', 'p'); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('padLeft and language helpers', async () => { + await setup(); + expect(String(component.padLeft.apply(3 as any))).toBe('03'); + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-examination-case-sheet/cancer-examination-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-examination-case-sheet/cancer-examination-case-sheet.component.spec.ts index 40d0b65c..3ee3d9d3 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-examination-case-sheet/cancer-examination-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-examination-case-sheet/cancer-examination-case-sheet.component.spec.ts @@ -20,27 +20,96 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { CancerExaminationCaseSheetComponent } from './cancer-examination-case-sheet.component'; describe('CancerExaminationCaseSheetComponent', () => { let component: CancerExaminationCaseSheetComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerExaminationCaseSheetComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerExaminationCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('language helpers set current language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('maps nurse examination data, image annotations and formats revisit date', () => { + component.caseSheetData = { + BeneficiaryData: { name: 'b' }, + nurseData: { + signsAndSymptoms: 's', + BenCancerLymphNodeDetails: 'l', + oralExamination: 'o', + breastExamination: 'br', + abdominalExamination: 'a', + gynecologicalExamination: 'g', + }, + ImageAnnotatedData: [{ imageID: 1 }, { imageID: 2, markers: [] }], + doctorData: { diagnosis: { revisitDate: '2024-07-08T00:00:00' } }, + }; + component.ngOnChanges(); + const t = new Date(); + expect(component.date).toBe( + t.getDate() + '/' + (t.getMonth() + 1) + '/' + t.getFullYear() + ); + expect(component.beneficiaryDetails).toEqual({ name: 'b' }); + expect(component.signsAndSymptoms).toBe('s'); + expect(component.BenCancerLymphNodeDetails).toBe('l'); + expect(component.oralExamination).toBe('o'); + expect(component.breastExamination).toBe('br'); + expect(component.abdominalExamination).toBe('a'); + expect(component.gynecologicalExamination).toBe('g'); + expect(component.diagnosisdetails.revisitDate).toBe('08/07/2024'); + expect(component.getImageAnnotation(2)).toEqual({ + imageID: 2, + markers: [], + }); + expect(component.getImageAnnotation(9)).toBeNull(); + }); + + it('keeps valid or missing revisit dates; no data sets only date', () => { + component.caseSheetData = { + nurseData: {}, + doctorData: { diagnosis: { revisitDate: '01/02/2024' } }, + }; + component.ngOnChanges(); + expect(component.beneficiaryDetails).toBeUndefined(); + expect(component.diagnosisdetails.revisitDate).toBe('01/02/2024'); + + component.caseSheetData = { nurseData: {}, doctorData: { diagnosis: {} } }; + component.ngOnChanges(); + expect(component.diagnosisdetails).toEqual({}); + + component.caseSheetData = null; + component.diagnosisdetails = 'prev'; + component.ngOnChanges(); + expect(component.diagnosisdetails).toBe('prev'); + expect(component.date).toBeDefined(); + }); + + it('padLeft pads single digits', () => { + expect(String(component.padLeft.apply(4 as any))).toBe('04'); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-history-case-sheet/cancer-history-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-history-case-sheet/cancer-history-case-sheet.component.spec.ts index 00ccd721..ff0e808b 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-history-case-sheet/cancer-history-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/cancer-history-case-sheet/cancer-history-case-sheet.component.spec.ts @@ -20,27 +20,71 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { CancerHistoryCaseSheetComponent } from './cancer-history-case-sheet.component'; describe('CancerHistoryCaseSheetComponent', () => { let component: CancerHistoryCaseSheetComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerHistoryCaseSheetComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerHistoryCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('language helpers set current language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('maps beneficiary, family, merged personal and obstetric history', () => { + const diet = { diet: 'veg' }; + component.caseSheetData = { + BeneficiaryData: { name: 'b' }, + nurseData: { + familyDiseaseHistory: ['f'], + benPersonalDietHistory: diet, + patientPersonalHistory: { tobacco: 'no' }, + patientObstetricHistory: { g: 1 }, + }, + }; + component.ngOnChanges(); + expect(component.beneficiaryDetails).toEqual({ name: 'b' }); + expect(component.familyDiseaseHistory).toEqual(['f']); + expect(component.patientPersonalHistory).toEqual({ + diet: 'veg', + tobacco: 'no', + }); + expect(diet).toEqual({ diet: 'veg' }); + expect(component.patientObstetricHistory).toEqual({ g: 1 }); + }); + + it('leaves fields unset for empty nurse data or no data', () => { + component.caseSheetData = { nurseData: {} }; + component.ngOnChanges(); + expect(component.beneficiaryDetails).toBeUndefined(); + expect(component.familyDiseaseHistory).toBeUndefined(); + expect(component.patientPersonalHistory).toBeUndefined(); + expect(component.patientObstetricHistory).toBeUndefined(); + component.caseSheetData = null; + expect(() => component.ngOnChanges()).not.toThrow(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/image-to-canvas/image-to-canvas.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/image-to-canvas/image-to-canvas.component.spec.ts index e258df30..95fbd011 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/image-to-canvas/image-to-canvas.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/cancer-case-sheet/image-to-canvas/image-to-canvas.component.spec.ts @@ -20,27 +20,107 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { ImageToCanvasComponent } from './image-to-canvas.component'; describe('ImageToCanvasComponent', () => { let component: ImageToCanvasComponent; let fixture: ComponentFixture; + let ctx: any; + let fakeImg: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ImageToCanvasComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ImageToCanvasComponent); component = fixture.componentInstance; - fixture.detectChanges(); + ctx = jasmine.createSpyObj('ctx', ['drawImage', 'strokeRect', 'fillText']); + component.canvas = { + nativeElement: { + width: 300, + height: 200, + getContext: jasmine.createSpy('getContext').and.returnValue(ctx), + }, + } as any; + fakeImg = { width: 50, height: 40 }; + spyOn(window as any, 'Image').and.returnValue(fakeImg); + }); + + it('language helpers set current language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('draws plain image when no markers are given', () => { + component.imgUrl = 'img.png'; + component.ngOnChanges(); + expect(component.canvas.nativeElement.getContext).toHaveBeenCalledWith( + '2d' + ); + expect(fakeImg.src).toBe('img.png'); + fakeImg.onload(); + expect(ctx.drawImage).toHaveBeenCalledWith(fakeImg, 0, 0, 250, 250); + }); + + it('annotates up to six markers, preferring x/yCord', () => { + const markers = [1, 2, 3, 4, 5, 6, 7].map(n => ({ + offsetX: n * 10, + offsetY: n * 10, + })); + (markers[0] as any).xCord = 100; + (markers[0] as any).yCord = 110; + component.imgUrl = 'a.png'; + component.annotatedMarker = { markers }; + component.ngOnChanges(); + expect(ctx.font).toBe('bold 20px serif'); + fakeImg.onload(); + expect(ctx.drawImage).toHaveBeenCalledWith( + fakeImg, + 0, + 0, + 50, + 40, + 0, + 0, + 300, + 200 + ); + expect(ctx.strokeRect).toHaveBeenCalledTimes(6); + expect(ctx.strokeRect.calls.first().args).toEqual([90, 100, 20, 20]); + expect(ctx.fillText.calls.first().args).toEqual([1, 97, 116]); + expect(ctx.fillText.calls.mostRecent().args[0]).toBe(6); + }); + + it('does nothing when marker object has no markers, or no image', () => { + component.imgUrl = 'a.png'; + component.annotatedMarker = {}; + component.ngOnChanges(); + component.imgUrl = ''; + component.annotatedMarker = null; + component.ngOnChanges(); + expect(component.canvas.nativeElement.getContext).not.toHaveBeenCalled(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('skips drawing when canvas has no 2d context support', () => { + component.canvas = { nativeElement: {} } as any; + component.loadImageOnCanvas('x'); + component.annotateImage([], 'x'); + expect(window.Image).not.toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/case-sheet.component.spec.ts index 07d80238..463f2646 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/case-sheet.component.spec.ts @@ -20,27 +20,253 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA, Injector } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; import { CaseSheetComponent } from './case-sheet.component'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; describe('CaseSheetComponent', () => { let component: CaseSheetComponent; let fixture: ComponentFixture; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + let mockInjector: jasmine.SpyObj; + let mockActivatedRoute: any; + + beforeEach(waitForAsync(() => { + mockHttpService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: new BehaviorSubject({ test: 'language' }), + }); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockActivatedRoute = { + snapshot: { + params: { + printablePage: 'current', + serviceType: '2', + }, + }, + }; + mockInjector = jasmine.createSpyObj('Injector', ['get']); + mockInjector.get.and.returnValue(null); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [CaseSheetComponent], + providers: [ + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(CaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should set serviceType from route params', () => { + fixture.detectChanges(); + expect(component.serviceType).toBe('2'); + }); + + it('should call fetchLanguageResponse', () => { + spyOn(component, 'fetchLanguageResponse'); + component.ngOnInit(); + expect(component.fetchLanguageResponse).toHaveBeenCalled(); + }); + + it('should call caseSheetCategory', () => { + spyOn(component, 'caseSheetCategory'); + component.ngOnInit(); + expect(component.caseSheetCategory).toHaveBeenCalled(); + }); + + it('should set previous and serviceType from dialog data when present', () => { + mockInjector.get.and.returnValue({ + previous: true, + serviceType: '5', + }); + (component as any).injector = mockInjector; + fixture.detectChanges(); + expect(mockInjector.get).toHaveBeenCalledWith(MAT_DIALOG_DATA, null); + expect(component.previous).toBeTrue(); + expect(component.serviceType).toBe('5'); + }); + + it('should not set previous or serviceType when dialog data is null', () => { + mockInjector.get.and.returnValue(null); + (component as any).injector = mockInjector; + fixture.detectChanges(); + expect(component.previous).toBeUndefined(); + expect(component.serviceType).toBe('2'); + }); + }); + + describe('caseSheetCategory', () => { + it('should set CancerScreening true when type is Cancer Screening (current dataStore)', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'Cancer Screening'; + return null; + }); + component.caseSheetCategory(); + expect(component.CancerScreening).toBeTrue(); + }); + + it('should set General true for General OPD (current dataStore)', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'General OPD'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for General OPD (QC)', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'General OPD (QC)'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for NCD care', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'NCD care'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for PNC', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'PNC'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for ANC', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'ANC'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for COVID-19 Screening', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'COVID-19 Screening'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should set General true for NCD screening', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'NCD screening'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should reset all flags for unknown type', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'caseSheetVisitCategory') return 'Unknown Type'; + return null; + }); + component.caseSheetCategory(); + expect(component.QC).toBeFalse(); + expect(component.CancerScreening).toBeFalse(); + expect(component.General).toBeFalse(); + }); + + it('should default to previous when printablePage is not set', () => { + mockActivatedRoute.snapshot.params = {}; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'previousCaseSheetVisitCategory') return 'ANC'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should use previousCaseSheetVisitCategory when dataStore is previous and previous is true', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'previous'; + component.previous = true; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'previousCaseSheetVisitCategory') return 'Cancer Screening'; + return null; + }); + component.caseSheetCategory(); + expect(component.CancerScreening).toBeTrue(); + }); + + it('should use previousCaseSheetVisitCategory when dataStore is previous and previous is falsy', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'previous'; + component.previous = undefined; + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'previousCaseSheetVisitCategory') return 'General OPD'; + return null; + }); + component.caseSheetCategory(); + expect(component.General).toBeTrue(); + }); + + it('should not set any boolean when type is undefined/null', () => { + mockActivatedRoute.snapshot.params['printablePage'] = 'current'; + mockSessionStorage.getItem.and.returnValue(null); + component.QC = false; + component.General = false; + component.CancerScreening = false; + component.caseSheetCategory(); + expect(component.QC).toBeFalse(); + expect(component.General).toBeFalse(); + expect(component.CancerScreening).toBeFalse(); + }); + }); + + describe('ngDoCheck', () => { + it('should call fetchLanguageResponse', () => { + spyOn(component, 'fetchLanguageResponse'); + component.ngDoCheck(); + expect(component.fetchLanguageResponse).toHaveBeenCalled(); + }); + }); + + describe('fetchLanguageResponse', () => { + it('should set currentLanguageSet from the language service', () => { + component.fetchLanguageResponse(); + expect(component.currentLanguageSet).toEqual({ test: 'language' }); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/anc-case-sheet/anc-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/anc-case-sheet/anc-case-sheet.component.spec.ts index e1fff8cd..26b1c971 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/anc-case-sheet/anc-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/anc-case-sheet/anc-case-sheet.component.spec.ts @@ -20,27 +20,52 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { AncCaseSheetComponent } from './anc-case-sheet.component'; describe('AncCaseSheetComponent', () => { let component: AncCaseSheetComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [AncCaseSheetComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(AncCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnInit and ngDoCheck set language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('maps ANC care details and immunization', () => { + component.caseSheetData = { + nurseData: { + anc: { ANCCareDetail: { lmp: 1 }, ANCWomenVaccineDetails: [{ v: 1 }] }, + }, + }; + component.ngOnChanges(); + expect(component.aNCDetailsAndFormula).toEqual({ lmp: 1 }); + expect(component.aNCImmunization).toEqual([{ v: 1 }]); + }); + + it('ignores data without ANC', () => { + component.caseSheetData = { nurseData: {} }; + component.ngOnChanges(); + expect(component.aNCDetailsAndFormula).toBeUndefined(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/doctor-diagnosis-case-sheet/doctor-diagnosis-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/doctor-diagnosis-case-sheet/doctor-diagnosis-case-sheet.component.spec.ts index d48e82d6..d4d5365c 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/doctor-diagnosis-case-sheet/doctor-diagnosis-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/doctor-diagnosis-case-sheet/doctor-diagnosis-case-sheet.component.spec.ts @@ -20,27 +20,550 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { DoctorDiagnosisCaseSheetComponent } from './doctor-diagnosis-case-sheet.component'; describe('DoctorDiagnosisCaseSheetComponent', () => { let component: DoctorDiagnosisCaseSheetComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let master: any; + let session: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async (sessionValues: Record = {}) => { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + master = autoSpy(MasterdataService); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + doctor.getAssessment.and.returnValue(of({ statusCode: 200, data: null })); + doctor.getUserId.and.returnValue(of({ userId: 77 })); + doctor.downloadSign.and.returnValue( + of(new Blob(['sig'], { type: 'image/png' })) + ); + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 200, data: null }) + ); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [DoctorDiagnosisCaseSheetComponent], + providers: [ + ...commonTestProviders({ + session: { + caseSheetVisitCategory: 'ANC', + caseSheetBeneficiaryRegID: '101', + visitCode: '555', + ...sessionValues, + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: MasterdataService, useValue: master }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DoctorDiagnosisCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + session = TestBed.inject(SessionStorageService); + spyOn(console, 'log'); + spyOn(console, 'error'); + }; + + const sheet = (over: any = {}): any => ({ + BeneficiaryData: { + serviceDate: '2024-03-05T08:09:10', + consultationDate: '2024-03-06T11:12:13', + ageVal: 30, + ...(over.ben || {}), + }, + nurseData: { + covidDetails: over.covid, + vitals: { + benAnthropometryDetail: { weight_Kg: 60 }, + benPhysicalVitalDetail: { + rbsTestResult: 110, + rbsTestRemarks: 'ok', + createdDate: 'd1', + }, + }, + anc: { lmp: 'x' }, + idrs: over.idrs, + }, + doctorData: over.doctorData || { + diagnosis: { + createdBy: 'drX', + ncdScreeningCondition: 'Diabetes||Hypertension', + complicationOfCurrentPregnancy: 'Anemia, Other-complications : null', + }, + LabReport: [{ procedureName: 'CBC' }], + Refer: { + revisitDate: '2024-04-01T00:00:00', + refrredToAdditionalServiceList: [], + }, + }, + }); + + describe('ngOnInit', () => { + it('fetches language, HRP status and assessment; no TC data by default', async () => { + await setup(); + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('ANC'); + expect(doctor.getHRPDetails).toHaveBeenCalledWith('101', '555'); + expect(component.showHRP).toBe('true'); + expect(doctor.getAssessment).toHaveBeenCalledWith('101'); + expect(doctor.getMMUCasesheetData).not.toHaveBeenCalled(); + expect(component.enableTCReferredMMUData).toBeFalse(); + }); + + it('sets showHRP false when not HRP and ignores non-200', async () => { + await setup(); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: false } }) + ); + component.fetchHRPPositive(); + expect(component.showHRP).toBe('false'); + component.showHRP = 'x'; + doctor.getHRPDetails.and.returnValue(of({ statusCode: 500 })); + component.fetchHRPPositive(); + expect(component.showHRP).toBe('x'); + }); + + it('loads MMU case sheet when caseSheetTMFlag is true', async () => { + await setup({ + caseSheetTMFlag: 'true', + caseSheetBenFlowID: 'f1', + caseSheetVisitID: 'v1', + caseSheetVisitCode: 'c1', + }); + doctor.getMMUCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.ngOnInit(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'ANC', + benFlowID: 'f1', + benVisitID: 'v1', + beneficiaryRegID: '101', + visitCode: 'c1', + }); + expect(component.enableTCReferredMMUData).toBeTrue(); + }); + + it('loads MMU case sheet when specialistFlag is 200', async () => { + await setup({ specialistFlag: '200' }); + doctor.getMMUCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.ngOnInit(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalled(); + expect(component.enableTCReferredMMUData).toBeTrue(); + }); + }); + + describe('getMMUCasesheetDataInTCReferred', () => { + beforeEach(async () => setup()); + + it('prepends RBS lab report, builds service list and refer details', () => { + doctor.getMMUCasesheetData.and.returnValue( + of({ + statusCode: 200, + data: { + nurseData: { + vitals: { + benPhysicalVitalDetail: { + rbsTestResult: 150, + rbsTestRemarks: 'high', + createdDate: 'd', + }, + }, + }, + doctorData: { + LabReport: [{ procedureName: 'X' }], + diagnosis: { createdBy: 'mmuDoc' }, + Refer: { + refrredToAdditionalServiceList: [ + { serviceName: 'A' }, + { serviceName: null }, + { serviceName: '' }, + { serviceName: 'B' }, + ], + }, + }, + }, + }) + ); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.MMUcaseRecords.LabReport.length).toBe(2); + expect(component.MMUcaseRecords.LabReport[0].procedureName).toBe( + 'RBS Test' + ); + expect( + component.MMUcaseRecords.LabReport[0].componentList[0].testResultValue + ).toBe(150); + expect(component.mmuServiceList).toBe('A,B'); + expect(component.userName).toBe('mmuDoc'); + expect(component.mmuCaseSheetData.referDetails).toEqual( + component.MMUReferDetails + ); + }); + + it('handles no RBS and no refer', () => { + doctor.getMMUCasesheetData.and.returnValue( + of({ + statusCode: 200, + data: { + nurseData: { vitals: { benPhysicalVitalDetail: {} } }, + doctorData: { LabReport: [] }, + }, + }) + ); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.MMUcaseRecords.LabReport).toEqual([]); + expect(component.mmuServiceList).toBe(''); + expect(component.mmuCaseSheetData.referDetails).toBeUndefined(); + }); + + it('ignores non-200 response', () => { + doctor.getMMUCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.mmuCaseSheetData).toBeUndefined(); + }); + }); + + describe('ngOnChanges', () => { + beforeEach(async () => setup()); + + it('does nothing without caseSheetData', () => { + component.ngOnChanges(); + expect(component.ncdScreeningCondition).toBeNull(); + expect(component.beneficiaryDetails).toBeUndefined(); + }); + + it('formats dates, vitals, ncd condition, complications, refer revisit date', () => { + component.visitCategory = 'ANC'; + component.caseSheetData = sheet(); + component.ngOnChanges(); + expect(component.userName).toBe('drX'); + expect(component.beneficiaryDetails.serviceDate).toBe( + '05/03/2024 08:09:10' + ); + expect(component.beneficiaryDetails.consultationDate).toBe( + '06/03/2024 11:12:13' + ); + expect(component.currentVitals.weight_Kg).toBe(60); + expect(component.ncdScreeningCondition).toBe('Diabetes,Hypertension'); + expect(component.caseRecords.LabReport[0].procedureName).toBe('RBS Test'); + expect(component.caseRecords.LabReport.length).toBe(2); + expect(component.tempComplication).toBeTrue(); + expect(component.newComp).toBe('Anemia'); + expect(component.referDetails.revisitDate).toBe('01/04/2024'); + expect(component.caseSheetData.referDetails).toBe(component.referDetails); + expect(component.ancDetails).toEqual({ lmp: 'x' }); + expect(master.getVaccinationTypeAndDoseMaster).toHaveBeenCalled(); + }); + + it('keeps a valid DD/MM/YYYY revisit date and no other complications', () => { + const s = sheet({ + doctorData: { + diagnosis: { complicationOfCurrentPregnancy: 'Anemia' }, + LabReport: [], + Refer: { revisitDate: '10/10/2024' }, + }, + ben: { serviceDate: null, consultationDate: null, ageVal: 5 }, + }); + s.nurseData.vitals.benPhysicalVitalDetail = {}; + component.caseSheetData = s; + component.ngOnChanges(); + expect(component.tempComplication).toBeFalse(); + expect(component.referDetails.revisitDate).toBe('10/10/2024'); + expect(component.caseRecords.LabReport).toEqual([]); + expect(component.ncdScreeningCondition).toBeNull(); + expect(master.getVaccinationTypeAndDoseMaster).not.toHaveBeenCalled(); + }); + + it('maps General OPD (QC) doctor data and service list', () => { + component.visitCategory = 'General OPD (QC)'; + component.caseSheetData = sheet({ + doctorData: { + findings: 'f', + prescription: 'p', + diagnosis: { + diagnosisProvided: 'dp', + instruction: 'ins', + externalInvestigation: 'ext', + }, + LabReport: [], + Refer: { + refrredToAdditionalServiceList: [ + { serviceName: 'S1' }, + { serviceName: '' }, + ], + }, + }, + }); + component.ngOnChanges(); + expect(component.caseRecords.diagnosis).toEqual({ + provisionalDiagnosis: 'dp', + specialistAdvice: 'ins', + externalInvestigation: 'ext', + }); + expect(component.caseRecords.findings).toBe('f'); + expect(component.serviceList).toBe('S1'); + }); + + it('reads covid details: symptoms, contacts, travel, suspected, recommendation', () => { + component.caseSheetData = sheet({ + covid: { + symptom: ['fever'], + contactStatus: ['c1'], + travelStatus: true, + suspectedStatusUI: 'Yes', + recommendation: [['r1', 'r2']], + }, + }); + component.ngOnChanges(); + expect(component.symptomsList).toEqual(['fever']); + expect(component.symptomFlag).toBeTrue(); + expect(component.contactFlag).toBeTrue(); + expect(component.travelFlag).toBeTrue(); + expect(component.travelStatus).toBe('Yes'); + expect(component.suspectedFlag).toBeTrue(); + expect(component.suspected).toBe('Yes'); + expect(component.recFlag).toBeTrue(); + expect(component.recommendationText).toBe('r1\nr2'); + }); + + it('handles travelStatus false, other travel value and empty contacts', () => { + component.caseSheetData = sheet({ + covid: { contactStatus: [], travelStatus: false }, + }); + component.ngOnChanges(); + expect(component.contactFlag).toBeFalse(); + expect(component.travelStatus).toBe('No'); + expect(component.travelFlag).toBeTrue(); + + component.caseSheetData = sheet({ covid: { travelStatus: 'maybe' } }); + component.ngOnChanges(); + expect(component.travelFlag).toBeFalse(); + }); + + it('extracts unique yes-answered IDRS questions and diseases', () => { + component.caseSheetData = sheet({ + idrs: { + IDRSDetail: { + idrsDetails: [ + { idrsQuestionId: 1, answer: 'yes' }, + { idrsQuestionId: 1, answer: 'yes' }, + { idrsQuestionId: 2, answer: 'no' }, + { idrsQuestionId: 3, answer: 'yes' }, + ], + suspectedDisease: 'A,B', + confirmedDisease: 'C', + }, + }, + }); + component.ngOnChanges(); + expect(component.temp1.map((x: any) => x.idrsQuestionId)).toEqual([1, 3]); + expect(component.suspect).toEqual(['A', 'B']); + expect(component.suspectt).toEqual(['C']); + expect(component.idrsScore).toBe( + component.caseSheetData.nurseData.idrs.IDRSDetail + ); + }); + + it('downloads signature when doctorSignatureFlag is set', () => { + spyOn(component, 'downloadSign'); + component.caseSheetData = sheet({ ben: { doctorSignatureFlag: true } }); + component.ngOnChanges(); + expect(component.downloadSign).toHaveBeenCalled(); + }); + }); + + describe('signature', () => { + beforeEach(async () => setup()); + + it('downloadSign uses tCSpecialistUserID when present', () => { + spyOn(component, 'showSign'); + component.beneficiaryDetails = { tCSpecialistUserID: 9 }; + component.userName = 'drX'; + component.downloadSign(); + expect(doctor.getUserId).toHaveBeenCalledWith('drX'); + expect(doctor.downloadSign).toHaveBeenCalledWith(9); + expect(component.showSign).toHaveBeenCalledWith(jasmine.any(Blob)); + }); + + it('downloadSign falls back to the fetched userId and logs errors', () => { + doctor.downloadSign.and.returnValue(throwingObs('x')); + component.beneficiaryDetails = {}; + component.downloadSign(); + expect(doctor.downloadSign).toHaveBeenCalledWith(77); + expect(console.error).toHaveBeenCalled(); + }); + + it('getUserId maps missing userId to null', done => { + doctor.getUserId.and.returnValue(of(null)); + component.getUserId().subscribe(v => { + expect(v).toBeNull(); + done(); + }); + }); + + it('showSign reads the blob into imgUrl', done => { + component.showSign(new Blob(['abc'], { type: 'text/plain' })); + setTimeout(() => { + expect(String(component.imgUrl)).toContain('data:text/plain'); + done(); + }, 50); + }); + }); + + describe('padLeft', () => { + beforeEach(async () => setup()); + it('pads single digits and leaves longer values', () => { + expect(String(component.padLeft.apply(5 as any))).toBe('05'); + expect(String(component.padLeft.apply(2024 as any))).toBe('2024'); + }); + }); + + describe('covid vaccination', () => { + beforeEach(async () => setup()); + + it('skips master fetch without beneficiary details or under 12', () => { + component.getVaccinationTypeAndDoseMaster(); + component.beneficiaryDetails = { ageVal: 10 }; + component.getVaccinationTypeAndDoseMaster(); + expect(master.getVaccinationTypeAndDoseMaster).not.toHaveBeenCalled(); + }); + + it('loads masters then previous vaccination with filtered types', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ + statusCode: 200, + data: { + doseType: [{ covidDoseTypeID: 1 }, { covidDoseTypeID: 2 }], + vaccineType: [{ covidVaccineTypeID: 5 }, { covidVaccineTypeID: 6 }], + }, + }) + ); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ + statusCode: 200, + data: { covidVSID: 3, doseTypeID: 2, covidVaccineTypeID: 5 }, + }) + ); + component.beneficiaryDetails = { ageVal: 20 }; + component.getVaccinationTypeAndDoseMaster(); + expect(master.getPreviousCovidVaccinationDetails).toHaveBeenCalledWith( + '101' + ); + expect(component.covidVaccineDetails.doseTypeID).toEqual([ + { covidDoseTypeID: 2 }, + ]); + expect(component.covidVaccineDetails.covidVaccineTypeID).toEqual([ + { covidVaccineTypeID: 5 }, + ]); + }); + + it('keeps raw ids when dose/vaccine ids are missing; ignores no covidVSID', () => { + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: { covidVSID: 3 } }) + ); + component.getPreviousCovidVaccinationDetails([], []); + expect(component.covidVaccineDetails).toEqual({ covidVSID: 3 }); + + component.covidVaccineDetails = undefined; + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getPreviousCovidVaccinationDetails([], []); + expect(component.covidVaccineDetails).toBeUndefined(); + }); + + it('logs errors and ignores non-200', () => { + const err = throwingObs({ errorMessage: 'e' }); + master.getVaccinationTypeAndDoseMaster.and.returnValue(err); + component.beneficiaryDetails = { ageVal: 20 }; + component.getVaccinationTypeAndDoseMaster(); + master.getPreviousCovidVaccinationDetails.and.returnValue(err); + component.getPreviousCovidVaccinationDetails([], []); + expect(console.log).toHaveBeenCalledWith('error', 'e'); + + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 500 }) + ); + master.getPreviousCovidVaccinationDetails.calls.reset(); + component.getVaccinationTypeAndDoseMaster(); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPreviousCovidVaccinationDetails([], []); + expect(component.covidVaccineDetails).toBeUndefined(); + }); + }); + + describe('assessment', () => { + beforeEach(async () => setup()); + + it('fetches details for last assessment id and enables result', () => { + doctor.getAssessment.and.returnValue( + of({ + statusCode: 200, + data: [{ assessmentId: 1 }, { assessmentId: 2 }], + }) + ); + doctor.getAssessmentDet.and.returnValue( + of({ + statusCode: 200, + data: { + severity: 'mild', + cough_pattern: 'dry', + cough_severity_score: 3, + record_duration: 10, + }, + }) + ); + component.getAssessmentID(); + expect(doctor.getAssessmentDet).toHaveBeenCalledWith(2); + expect(component.severity).toBe('mild'); + expect(component.cough_pattern).toBe('dry'); + expect(component.cough_severity_score).toBe(3); + expect(component.record_duration).toBe(10); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + expect(component.enableResult).toBeTrue(); + }); + + it('skips details when assessment id is null or response empty', () => { + doctor.getAssessment.and.returnValue( + of({ statusCode: 200, data: [{ assessmentId: null }] }) + ); + component.getAssessmentID(); + expect(doctor.getAssessmentDet).not.toHaveBeenCalled(); + doctor.getAssessmentDet.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.getAssessmentDetails(1); + expect(component.enableResult).toBeFalse(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', async () => { + await setup(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/examination-case-sheet/examination-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/examination-case-sheet/examination-case-sheet.component.spec.ts index b08ea25c..03b6f3bb 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/examination-case-sheet/examination-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/examination-case-sheet/examination-case-sheet.component.spec.ts @@ -20,27 +20,110 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { ExaminationCaseSheetComponent } from './examination-case-sheet.component'; describe('ExaminationCaseSheetComponent', () => { let component: ExaminationCaseSheetComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ExaminationCaseSheetComponent], + providers: [ + ...commonTestProviders({ + session: { + caseSheetVisitCategory: 'General OPD', + beneficiaryRegID: '5', + visitID: '6', + }, + }), + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ExaminationCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + spyOn(console, 'log'); + }); + + it('ngOnInit reads language and session values', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('General OPD'); + expect(component.beneficiaryRegID).toBe('5'); + expect(component.visitID).toBe('6'); + }); + + it('maps every examination section', () => { + component.caseSheetData = { + nurseData: { + examination: { + generalExamination: 'g', + headToToeExamination: 'h', + cardiovascularExamination: 'c', + respiratoryExamination: 'r', + centralNervousExamination: 'cn', + musculoskeletalExamination: 'm', + genitourinaryExamination: 'gu', + obstetricExamination: 'o', + gastrointestinalExamination: 'gi', + }, + }, + }; + component.ngOnChanges(); + expect(component.generalExamination).toBe('g'); + expect(component.headToToeExamination).toBe('h'); + expect(component.cardioVascularExamination).toBe('c'); + expect(component.respiratorySystemExamination).toBe('r'); + expect(component.centralNervousSystemExamination).toBe('cn'); + expect(component.musculoskeletalSystemExamination).toBe('m'); + expect(component.genitoUrinarySystemExamination).toBe('gu'); + expect(component.obstetricExamination).toBe('o'); + expect(component.gastroIntestinalExamination).toBe('gi'); + expect(component.referDetails).toBeUndefined(); + }); + + it('leaves sections undefined for empty examination', () => { + component.caseSheetData = { nurseData: { examination: {} } }; + component.ngOnChanges(); + expect(component.generalExamination).toBeUndefined(); + expect(component.gastroIntestinalExamination).toBeUndefined(); + }); + + it('builds refer service list skipping empty names', () => { + component.caseSheetData = { + doctorData: { + Refer: { + refrredToAdditionalServiceList: [ + { serviceName: 'X' }, + { serviceName: '' }, + { serviceName: 'Y' }, + ], + }, + }, + }; + component.ngOnChanges(); + expect(component.serviceList).toBe('X,Y'); + }); + + it('handles doctor data without Refer and no data at all', () => { + component.caseSheetData = { doctorData: {} }; + component.ngOnChanges(); + expect(component.referDetails).toBeUndefined(); + expect(component.serviceList).toBe(''); + component.caseSheetData = undefined; + expect(() => component.ngOnChanges()).not.toThrow(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', () => { + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/general-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/general-case-sheet.component.spec.ts index 30a2d5c4..4ac9e5c3 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/general-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/general-case-sheet.component.spec.ts @@ -20,27 +20,299 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Location } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { NurseService } from '../../shared/services'; +import { PrintPageSelectComponent } from '../../print-page-select/print-page-select.component'; +import { PrescribeTmMedicineComponent } from '../prescribe-tm-medicine/prescribe-tm-medicine.component'; import { GeneralCaseSheetComponent } from './general-case-sheet.component'; describe('GeneralCaseSheetComponent', () => { let component: GeneralCaseSheetComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let location: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async (session: Record = {}, params: any = {}) => { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + location = { back: jasmine.createSpy('back') }; + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralCaseSheetComponent], + providers: [ + ...commonTestProviders({ session }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: Location, useValue: location }, + { provide: ActivatedRoute, useValue: { snapshot: { params } } }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(GeneralCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + confirm.alert.and.callFake(() => createDialogRefMock()); + dialog = TestBed.inject(MatDialog); + spyOn(console, 'log'); + }; + + const current = { + caseSheetVisitCategory: 'ANC', + caseSheetBenFlowID: 'f', + caseSheetVisitID: 'v', + caseSheetBeneficiaryRegID: 'r', + caseSheetVisitCode: 'cv', + visitCode: 'vc', + }; + + describe('ngOnInit', () => { + it('defaults to previous case sheet and requests TM data', async () => { + await setup({ + previousCaseSheetVisitCategory: 'PNC', + previousCaseSheetBenFlowID: 'pf', + previousCaseSheetBeneficiaryRegID: 'pr', + previousCaseSheetVisitCode: 'pv', + }); + component.serviceType = 'TM'; + doctor.getTMCasesheetData.and.returnValue( + of({ statusCode: 200, data: { a: 1 } }) + ); + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.dataStore).toBe('previous'); + expect(component.hideBack).toBeTrue(); + expect(component.visitCategory).toBe('PNC'); + expect(doctor.getTMCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'PNC', + benFlowID: 'pf', + beneficiaryRegID: 'pr', + visitCode: 'pv', + }); + expect(component.caseSheetData).toEqual({ a: 1 }); + }); + + it('current page with MMU service fetches MMU casesheet', async () => { + await setup(current, { printablePage: 'current' }); + component.serviceType = 'MMU'; + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: { b: 2 } }) + ); + component.ngOnInit(); + expect(component.hideBack).toBeFalse(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'ANC', + benFlowID: 'f', + benVisitID: 'v', + beneficiaryRegID: 'r', + visitCode: 'vc', + }); + expect(component.caseSheetData).toEqual({ b: 2 }); + expect(component.hideSelectQC).toBeFalse(); + }); + + it('TM flag (boolean true) enables prescription and loads TM referred data', async () => { + await setup({ ...current, caseSheetTMFlag: true }); + nurse.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 200, data: { c: 3 } }) + ); + component.ngOnInit(); + expect(component.enablePrescriptionButton).toBeTrue(); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'ANC', + benFlowID: 'f', + benVisitID: 'v', + beneficiaryRegID: 'r', + visitCode: 'cv', + }); + expect(component.caseSheetData).toEqual({ c: 3 }); + }); + + it('specialistFlag 200 loads TM referred data without prescription button', async () => { + await setup({ ...current, specialistFlag: '200' }); + nurse.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.ngOnInit(); + expect(component.enablePrescriptionButton).toBeFalse(); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalled(); + }); + + it('unknown page does nothing', async () => { + await setup({}, { printablePage: 'other' }); + component.ngOnInit(); + expect(doctor.getTMCasesheetData).not.toHaveBeenCalled(); + expect(doctor.getMMUCasesheetData).not.toHaveBeenCalled(); + }); + }); + + describe('getTMReferredCasesheetData', () => { + beforeEach(async () => setup()); + + it('alerts and goes back on failure response', () => { + nurse.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.getTMReferredCasesheetData({}); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + expect(location.back).toHaveBeenCalled(); + }); + + it('alerts and goes back on error', () => { + nurse.getTMReferredCasesheetData.and.returnValue(throwingObs()); + component.getTMReferredCasesheetData({}); + expect(confirm.alert).toHaveBeenCalledWith( + 'Error in fetching TM Casesheet', + 'error' + ); + expect(location.back).toHaveBeenCalled(); + }); + }); + + describe('getCasesheetData', () => { + beforeEach(async () => setup()); + + it('hides QC select for QC visits or previous view; ignores failures', () => { + component.visitCategory = 'General OPD (QC)'; + component.serviceType = 'TM'; + doctor.getTMCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.getCasesheetData({}); + expect(component.hideSelectQC).toBeTrue(); + expect(component.caseSheetData).toBeUndefined(); + + component.hideSelectQC = false; + component.visitCategory = 'ANC'; + component.previous = true; + component.serviceType = 'MMU'; + doctor.getMMUCasesheetData.and.returnValue(of(null)); + component.getCasesheetData({}); + expect(component.hideSelectQC).toBeTrue(); + expect(component.caseSheetData).toBeUndefined(); + }); + }); + + describe('selectPrintPage', () => { + beforeEach(async () => setup()); + + it('opens dialog and applies returned selection', () => { + const result = { + caseSheetANC: false, + caseSheetPNC: false, + caseSheetExamination: true, + caseSheetHistory: false, + caseSheetCovidVaccinationDetails: false, + }; + dialog.open.and.returnValue(createDialogRefMock(result)); + component.visitCategory = 'ANC'; + component.selectPrintPage(); + expect(dialog.open).toHaveBeenCalledWith(PrintPageSelectComponent, { + width: '420px', + disableClose: false, + data: { + printPagePreviewSelect: component.printPagePreviewSelect, + visitCategory: 'ANC', + }, + }); + expect(component.printPagePreviewSelect).toEqual(result); + }); + + it('keeps selection when dialog dismissed', () => { + component.selectPrintPage(); + expect(component.printPagePreviewSelect.caseSheetANC).toBeTrue(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('prescribeTMMedicine', () => { + beforeEach(async () => setup()); + + it('opens prescribe dialog and stores result drugs', () => { + dialog.open.and.returnValue( + createDialogRefMock({ prescribedDrugs: [{ id: 1 }] }) + ); + component.caseSheetData = { doctorData: { prescription: [{ id: 9 }] } }; + component.prescribeTMMedicine(); + expect(dialog.open).toHaveBeenCalledWith(PrescribeTmMedicineComponent, { + data: { + height: '560px', + weight: '680px', + disableClose: true, + tmPrescribedDrugs: [{ id: 9 }], + }, + }); + expect(doctor.prescribedDrugData).toEqual([{ id: 1 }]); + }); + + it('logs when dialog closed without result', () => { + component.caseSheetData = { doctorData: { prescription: [{ id: 9 }] } }; + component.prescribeTMMedicine(); + expect(console.log).toHaveBeenCalledWith('No prescribed drugs'); + }); + + it('alerts when there are no prescriptions', () => { + component.caseSheetData = { doctorData: { prescription: [] } }; + component.prescribeTMMedicine(); + component.caseSheetData = null; + component.prescribeTMMedicine(); + expect(dialog.open).not.toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + 'There is no prescribed drugs from TM specialist' + ); + }); + }); + + describe('navigation and browser helpers', () => { + beforeEach(async () => setup()); + + it('downloadCasesheet prints', () => { + const print = spyOn(window, 'print'); + component.downloadCasesheet(); + expect(print).toHaveBeenCalled(); + }); + + it('goToTop scrolls to top', () => { + const scroll = spyOn(window, 'scrollTo'); + component.goToTop(); + expect(scroll).toHaveBeenCalledWith(0, 0); + }); + + it('goBackVisitDet alerts and goes back', () => { + component.goBackVisitDet(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Error in fetching TM Casesheet', + 'error' + ); + expect(location.back).toHaveBeenCalled(); + }); + + it('ngOnDestroy unsubscribes', () => { + const sub = { unsubscribe: jasmine.createSpy('u') }; + component.casesheetSubs = sub; + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + component.casesheetSubs = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('ngDoCheck refreshes language', () => { + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/history-case-sheet/history-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/history-case-sheet/history-case-sheet.component.spec.ts index b60196a1..5ac72737 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/history-case-sheet/history-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/history-case-sheet/history-case-sheet.component.spec.ts @@ -20,27 +20,215 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { DoctorService } from '../../../shared/services'; import { HistoryCaseSheetComponent } from './history-case-sheet.component'; describe('HistoryCaseSheetComponent', () => { let component: HistoryCaseSheetComponent; let fixture: ComponentFixture; + let doctor: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const setup = async (session: Record = {}) => { + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [HistoryCaseSheetComponent], + providers: [ + ...commonTestProviders({ + session: { caseSheetVisitCategory: 'General OPD', ...session }, + }), + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(HistoryCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); + spyOn(console, 'log'); + }; + + const services = [ + { serviceName: 'A' }, + { serviceName: null }, + { serviceName: 'B' }, + ]; + + describe('ngOnInit', () => { + it('sets language and visit category without TC flag', async () => { + await setup(); + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('General OPD'); + expect(component.enableTCReferredMMUData).toBeFalse(); + expect(doctor.getMMUCasesheetData).not.toHaveBeenCalled(); + }); + + it('requests MMU data when caseSheetTMFlag is "true"', async () => { + await setup({ + caseSheetTMFlag: 'true', + caseSheetBenFlowID: 'f', + caseSheetVisitID: 'v', + caseSheetBeneficiaryRegID: 'r', + caseSheetVisitCode: 'c', + }); + component.ngOnInit(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'General OPD', + benFlowID: 'f', + benVisitID: 'v', + beneficiaryRegID: 'r', + visitCode: 'c', + }); + expect(component.enableTCReferredMMUData).toBeTrue(); + }); + + it('requests MMU data when specialistFlag is 200', async () => { + await setup({ specialistFlag: '200' }); + component.ngOnInit(); + expect(doctor.getMMUCasesheetData).toHaveBeenCalled(); + }); + }); + + describe('getMMUCasesheetDataInTCReferred', () => { + beforeEach(async () => setup()); + + it('builds comma-separated service list and refer details', () => { + doctor.getMMUCasesheetData.and.returnValue( + of({ + statusCode: 200, + data: { + doctorData: { Refer: { refrredToAdditionalServiceList: services } }, + }, + }) + ); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.mmuServiceList).toBe('A,B'); + expect(component.mmuCaseSheetData.referDetails).toEqual({ + refrredToAdditionalServiceList: services, + }); + }); + + it('handles missing Refer and missing doctorData', () => { + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: { doctorData: {} } }) + ); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.mmuServiceList).toBe(''); + expect(component.mmuCaseSheetData.referDetails).toBeUndefined(); + + doctor.getMMUCasesheetData.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.mmuCaseSheetData).toEqual({}); + }); + + it('ignores non-200', () => { + doctor.getMMUCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.getMMUCasesheetDataInTCReferred({}); + expect(component.mmuCaseSheetData).toBeUndefined(); + }); + }); + + describe('ngOnChanges', () => { + beforeEach(async () => setup()); + + const fullHistory = () => ({ + PastHistory: { pastIllness: ['pi'], pastSurgery: ['ps'] }, + FamilyHistory: { f: 1 }, + PhysicalActivityHistory: { p: 1 }, + childOptionalVaccineHistory: { childOptionalVaccineList: ['cv'] }, + ComorbidityConditions: { comorbidityConcurrentConditionsList: ['cc'] }, + MedicationHistory: { medicationHistoryList: ['mh'] }, + FemaleObstetricHistory: { fo: 1 }, + DevelopmentHistory: { d: 1 }, + FeedingHistory: { fe: 1 }, + MenstrualHistory: { m: 1 }, + PerinatalHistory: { pe: 1 }, + PersonalHistory: { pers: 1 }, + ImmunizationHistory: { im: 1 }, + }); + + it('maps every history section, ANC details and refer info', () => { + component.caseSheetData = { + BeneficiaryData: { name: 'x' }, + nurseData: { history: fullHistory(), anc: { ANCCareDetail: { a: 1 } } }, + doctorData: { + Refer: { + refrredToAdditionalServiceList: services, + revisitDate: '2024-02-03T00:00:00', + }, + }, + }; + component.ngOnChanges(); + expect(component.beneficiary).toEqual({ name: 'x' }); + expect(component.ANCDetailsAndFormula).toEqual({ a: 1 }); + expect(component.pastIllnessList).toEqual(['pi']); + expect(component.pastSurgeryList).toEqual(['ps']); + expect(component.familyHistory).toEqual({ f: 1 }); + expect(component.previousPhysicalList).toEqual({ p: 1 }); + expect(component.childOptionalVaccineList).toEqual(['cv']); + expect(component.comorbidConditionList).toEqual(['cc']); + expect(component.medicationHistoryList).toEqual(['mh']); + expect(component.femaleObstetricHistory).toEqual({ fo: 1 }); + expect(component.developmentalHistory).toEqual({ d: 1 }); + expect(component.feedingHistory).toEqual({ fe: 1 }); + expect(component.menstrualHistory).toEqual({ m: 1 }); + expect(component.perinatalHistory).toEqual({ pe: 1 }); + expect(component.personalHistory).toEqual({ pers: 1 }); + expect(component.immunizationHistory).toEqual({ im: 1 }); + expect(component.serviceList).toBe('A,B'); + expect(component.referDetails.revisitDate).toBe('03/02/2024'); + expect(component.caseSheetData.referDetails).toBe(component.referDetails); + }); + + it('leaves sections unset when history is empty and keeps valid revisit date', () => { + component.caseSheetData = { + BeneficiaryData: {}, + nurseData: { history: {} }, + doctorData: { Refer: { revisitDate: '05/06/2024' } }, + }; + component.ngOnChanges(); + expect(component.pastIllnessList).toBeUndefined(); + expect(component.ANCDetailsAndFormula).toBeUndefined(); + expect(component.immunizationHistory).toBeUndefined(); + expect(component.referDetails.revisitDate).toBe('05/06/2024'); + }); + + it('skips everything without beneficiary or doctor data', () => { + component.caseSheetData = { nurseData: { history: fullHistory() } }; + component.ngOnChanges(); + expect(component.beneficiary).toBeUndefined(); + expect(component.referDetails).toBeUndefined(); + + component.caseSheetData = { + BeneficiaryData: {}, + nurseData: {}, + doctorData: {}, + }; + component.ngOnChanges(); + expect(component.generalhistory).toBeUndefined(); + expect(component.caseSheetData.referDetails).toBeUndefined(); + }); + }); + + it('padLeft pads to two digits', async () => { + await setup(); + expect(String(component.padLeft.apply(7 as any))).toBe('07'); + expect(String(component.padLeft.apply(12 as any))).toBe('12'); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', async () => { + await setup(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/pnc-case-sheet/pnc-case-sheet.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/pnc-case-sheet/pnc-case-sheet.component.spec.ts index 81d61006..f3507e89 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/pnc-case-sheet/pnc-case-sheet.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/general-case-sheet/pnc-case-sheet/pnc-case-sheet.component.spec.ts @@ -20,27 +20,49 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; import { PncCaseSheetComponent } from './pnc-case-sheet.component'; describe('PncCaseSheetComponent', () => { let component: PncCaseSheetComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [PncCaseSheetComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(PncCaseSheetComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnInit and ngDoCheck set language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('maps PNC care details', () => { + component.caseSheetData = { + nurseData: { pnc: { PNCCareDetail: { d: 1 } } }, + }; + component.ngOnChanges(); + expect(component.pNCCaseSheetData).toEqual({ d: 1 }); + }); + + it('ignores data without PNC', () => { + component.caseSheetData = {}; + component.ngOnChanges(); + expect(component.pNCCaseSheetData).toBeUndefined(); }); }); diff --git a/src/app/app-modules/nurse-doctor/case-sheet/prescribe-tm-medicine/prescribe-tm-medicine.component.spec.ts b/src/app/app-modules/nurse-doctor/case-sheet/prescribe-tm-medicine/prescribe-tm-medicine.component.spec.ts index da58a7e8..bb87b753 100644 --- a/src/app/app-modules/nurse-doctor/case-sheet/prescribe-tm-medicine/prescribe-tm-medicine.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/case-sheet/prescribe-tm-medicine/prescribe-tm-medicine.component.spec.ts @@ -20,27 +20,369 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormArray } from '@angular/forms'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { + DoctorService, + MasterdataService, +} from '../../../nurse-doctor/shared/services'; import { PrescribeTmMedicineComponent } from './prescribe-tm-medicine.component'; describe('PrescribeTmMedicineComponent', () => { let component: PrescribeTmMedicineComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let confirm: any; + let dialogRef: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PrescribeTmMedicineComponent], - }).compileComponents(); - })); + const masterData = { + drugFormMaster: [{ itemFormID: 1, itemFormName: 'Tablet' }], + itemMaster: [ + { itemID: 10, itemName: 'Paracetamol', itemFormID: 1, quantityInHand: 5 }, + { itemID: 11, itemName: 'Syrup', itemFormID: 2 }, + ], + drugDoseMaster: [ + { itemFormID: 1, dose: '1' }, + { itemFormID: 2, dose: '5ml' }, + ], + drugFrequencyMaster: ['OD'], + drugDurationUnitMaster: ['Day'], + routeOfAdmin: ['Oral'], + NonEdlMaster: [ + { itemID: 20, itemName: 'Pantoprazole', itemFormID: 1 }, + { itemID: 21, itemName: 'Other', itemFormID: 3 }, + ], + }; - beforeEach(() => { + beforeEach(async () => { + master = autoSpy(MasterdataService); + doctor = autoSpy(DoctorService); + master.getDoctorMasterDataForNurse.and.returnValue( + of({ statusCode: 200, data: masterData }) + ); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PrescribeTmMedicineComponent], + providers: [ + ...commonTestProviders({ + session: { + userName: 'nurse1', + caseSheetVisitCategoryID: '4', + providerServiceID: '7', + serviceLineDetails: JSON.stringify({ vanID: 3, parkingPlaceID: 9 }), + }, + dialogData: { tmPrescribedDrugs: [{ drugID: 99 }] }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PrescribeTmMedicineComponent, '') + .compileComponents(); fixture = TestBed.createComponent(PrescribeTmMedicineComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialogRef = TestBed.inject(MatDialogRef); + spyOn(console, 'log'); + component.ngOnInit(); + component.prescriptionForm = { + form: { markAsUntouched: jasmine.createSpy('markAsUntouched') }, + } as any; + }); + + const drugs = () => + component.drugPrescriptionForm.controls['prescribedDrugs'] as FormArray; + + it('ngOnInit initialises form, limits, durations and masters', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.tmPrescribedDrugs).toEqual([{ drugID: 99 }]); + expect(component.createdBy).toBe('nurse1'); + expect(component.drugPrescriptionForm.value).toEqual({ + vanID: 3, + parkingPlaceID: 9, + prescribedDrugs: [], + }); + expect(component.pageLimits).toEqual([0, 5]); + expect(component.drugDurationMaster.length).toBe(29); + expect(component.drugDurationMaster[28]).toBe(29); + expect(master.getDoctorMasterDataForNurse).toHaveBeenCalledWith('4', '7'); + expect(component.drugFormMaster).toBe(masterData.drugFormMaster); + expect(component.drugRouteMaster).toEqual(['Oral']); + expect(component.edlMaster).toBe(masterData.NonEdlMaster); + }); + + it('keeps masters empty on non-200', () => { + component.drugFormMaster = []; + master.getDoctorMasterDataForNurse.and.returnValue(of({ statusCode: 500 })); + component.getDoctorMasterData(); + expect(component.drugFormMaster).toEqual([]); + }); + + it('setLimits computes page window', () => { + component.setLimits(2); + expect(component.pageLimits).toEqual([10, 15]); + }); + + it('getFormValueChanged filters drugs and doses by form', () => { + component.tempform = { itemFormID: 1, itemFormName: 'Tablet' }; + component.getFormValueChanged(); + expect(component.currentPrescription.formID).toBe(1); + expect(component.currentPrescription.formName).toBe('Tablet'); + expect(component.filteredDrugMaster.map((d: any) => d.itemID)).toEqual([ + 10, 20, + ]); + expect(component.filteredDrugMaster[0].isEDL).toBeTrue(); + expect(component.filteredDrugMaster[1].quantityInHand).toBe(0); + expect(component.subFilteredDrugMaster).toBe(component.filteredDrugMaster); + expect(component.filteredDrugDoseMaster).toEqual([ + { itemFormID: 1, dose: '1' }, + ]); + expect(component.prescriptionForm.form.markAsUntouched).toHaveBeenCalled(); + }); + + it('filterMedicine filters by prefix, case-insensitive, and resets on empty', () => { + component.filteredDrugMaster = [ + { itemName: 'Paracetamol' }, + { itemName: 'Pantoprazole' }, + { itemName: 'Aspirin' }, + ]; + component.filterMedicine('PARA'); + expect(component.subFilteredDrugMaster).toEqual([ + { itemName: 'Paracetamol' }, + ]); + component.filterMedicine(''); + expect(component.subFilteredDrugMaster).toBe(component.filteredDrugMaster); + }); + + describe('reEnterMedicine', () => { + it('restores the selected drug object when a drug is selected', () => { + component.tempDrugName = 'typed'; + Object.assign(component.currentPrescription, { + id: 1, + drugName: 'Para', + drugID: 10, + quantity: 4, + sctCode: 's', + sctTerm: 't', + drugStrength: '500', + drugUnit: 'mg', + }); + component.reEnterMedicine(); + expect(component.tempDrugName).toEqual({ + id: 1, + itemName: 'Para', + itemID: 10, + quantityInHand: 4, + sctCode: 's', + sctTerm: 't', + strength: '500', + unitOfMeasurement: 'mg', + }); + }); + + it('clears typed text when no drug selected', () => { + component.tempDrugName = 'typed'; + component.reEnterMedicine(); + expect(component.tempDrugName).toBeNull(); + }); + + it('resets details and refilters when nothing typed', () => { + component.tempform = { itemFormID: 1, itemFormName: 'Tablet' }; + component.tempDrugName = null; + component.reEnterMedicine(); + expect(component.currentPrescription.formID).toBe(1); + expect(component.filteredDrugMaster.length).toBe(2); + }); + }); + + it('displayFn formats options', () => { + expect( + component.displayFn({ + itemName: 'Para', + strength: '500', + unitOfMeasurement: 'mg', + quantityInHand: 3, + }) + ).toBe('Para 500mg(3)'); + expect(component.displayFn({ itemName: 'X', strength: '1' })).toBe('X 1'); + expect(component.displayFn(null)).toBe(''); + }); + + describe('selectMedicineObject', () => { + const opt = (over: any = {}) => ({ + id: 1, + itemName: 'Para', + itemID: 10, + quantityInHand: 5, + sctCode: 's', + sctTerm: 't', + strength: '500', + unitOfMeasurement: 'mg', + isEDL: true, + ...over, + }); + + it('ignores non-user input', () => { + component.selectMedicineObject({ + source: { value: opt() }, + isUserInput: false, + }); + expect(component.currentPrescription.drugID).toBeNull(); + }); + + it('selects an in-stock drug as primary', () => { + component.selectMedicineObject({ + source: { value: opt() }, + isUserInput: true, + }); + expect(component.currentPrescription.drugID).toBe(10); + expect(component.currentPrescription.drugName).toBe('Para'); + expect(component.currentPrescription.isEDL).toBeTrue(); + expect(component.isStockAvalable).toBe('primary'); + expect(confirm.confirm).not.toHaveBeenCalled(); + }); + + it('asks to confirm out-of-stock non-EDL drug; accepted -> warn', () => { + component.selectMedicineObject({ + source: { value: opt({ quantityInHand: 0, isEDL: false }) }, + isUserInput: true, + }); + expect(confirm.confirm).toHaveBeenCalledWith( + 'info - (Non-EDL) Medicine', + 'Stock not Available, would you still like to prescribe? Para 500mg' + ); + expect(component.isStockAvalable).toBe('warn'); + expect(component.currentPrescription.drugID).toBe(10); + }); + + it('out-of-stock declined clears the selection', () => { + confirm.confirm.and.returnValue(of(false)); + component.tempDrugName = 'x'; + component.selectMedicineObject({ + source: { value: opt({ quantityInHand: 0 }) }, + isUserInput: true, + }); + expect(confirm.confirm.calls.mostRecent().args[0]).toBe('info '); + expect(component.tempDrugName).toBeNull(); + expect(component.currentPrescription.drugID).toBe(''); + expect(component.isStockAvalable).toBe(''); + }); + + it('alerts when drug already prescribed', () => { + component.currentPrescription.drugStrength = '500'; + component.currentPrescription.drugID = 10; + component.addMedicine(); + component.currentPrescription.drugID = null; + spyOn(component, 'reEnterMedicine'); + component.selectMedicineObject({ + source: { value: opt() }, + isUserInput: true, + }); + expect(component.reEnterMedicine).toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Medicine is already prescribed, Please delete the previously added one to change.', + 'info' + ); + expect(component.currentPrescription.drugID).toBeNull(); + }); + }); + + it('submitForUpload adds medicine with createdBy and clears', () => { + Object.assign(component.currentPrescription, { + drugID: 10, + drugName: 'Para', + drugStrength: '500', + drugUnit: 'mg', + duration: 3, + unit: 'Day', + }); + component.tempform = { itemFormID: 1 }; + component.submitForUpload(); + expect(drugs().length).toBe(1); + expect(drugs().at(0).value.drugStrength).toBe('500mg'); + expect(drugs().at(0).value.createdBy).toBe('nurse1'); + expect(drugs().at(0).value.durationView).toBe('3 Day'); + expect(component.tempform).toBeNull(); + expect(component.currentPrescription.drugID).toBeNull(); + expect(component.getPrescribedDrugs()?.length).toBe(1); + }); + + it('getPrescribedDrugs returns null when control is not an array', () => { + component.drugPrescriptionForm.removeControl('prescribedDrugs'); + expect(component.getPrescribedDrugs()).toBeNull(); + }); + + describe('deleteMedicine', () => { + beforeEach(() => { + component.currentPrescription.drugID = 1; + component.addMedicine(); + component.currentPrescription.drugID = 2; + component.addMedicine(); + }); + + it('removes in UI when no id', () => { + component.deleteMedicine(0); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + 'Please confirm to delete.' + ); + expect(drugs().length).toBe(1); + expect(drugs().at(0).value.drugID).toBe(1); + expect(doctor.deleteMedicine).not.toHaveBeenCalled(); + }); + + it('deletes in backend then UI when id given', () => { + doctor.deleteMedicine.and.returnValue(of({ statusCode: 200 })); + component.deleteMedicine(1, 55 as any); + expect(doctor.deleteMedicine).toHaveBeenCalledWith(55); + expect(drugs().length).toBe(1); + }); + + it('keeps row when backend fails', () => { + doctor.deleteMedicine.and.returnValue(of({ statusCode: 500 })); + component.deleteMedicine(1, 55 as any); + expect(drugs().length).toBe(2); + }); + + it('does nothing when not confirmed', () => { + confirm.confirm.and.returnValue(of(false)); + component.deleteMedicine(0, 55 as any); + component.deleteMedicine(0); + expect(drugs().length).toBe(2); + }); + }); + + it('submitPrescription closes dialog with form value', () => { + component.submitPrescription(); + expect(dialogRef.close).toHaveBeenCalledWith( + component.drugPrescriptionForm.value + ); + }); + + it('submitPrescription alerts when there is no value', () => { + component.drugPrescriptionForm = { value: null } as any; + component.submitPrescription(); + expect(dialogRef.close).not.toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Please prescribe the medicines' + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/dashboard/dashboard.component.spec.ts b/src/app/app-modules/nurse-doctor/dashboard/dashboard.component.spec.ts index bd7f10ea..710a2658 100644 --- a/src/app/app-modules/nurse-doctor/dashboard/dashboard.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/dashboard/dashboard.component.spec.ts @@ -20,27 +20,31 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DashboardComponent } from './dashboard.component'; -describe('DashboardComponent', () => { +describe('nurse-doctor DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [DashboardComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('renders the shell: header with roles, router outlet and footer', () => { expect(component).toBeTruthy(); + const el: HTMLElement = fixture.nativeElement; + const header = el.querySelector('app-header'); + expect(header).not.toBeNull(); + expect(el.querySelector('router-outlet')).not.toBeNull(); + expect(el.querySelector('app-footer')).not.toBeNull(); }); }); diff --git a/src/app/app-modules/nurse-doctor/doctor-worklist/doctor-worklist.component.spec.ts b/src/app/app-modules/nurse-doctor/doctor-worklist/doctor-worklist.component.spec.ts index 6892c2a4..9c8033bb 100644 --- a/src/app/app-modules/nurse-doctor/doctor-worklist/doctor-worklist.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/doctor-worklist/doctor-worklist.component.spec.ts @@ -20,17 +20,101 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { of, throwError } from 'rxjs'; import { DoctorWorklistComponent } from './doctor-worklist.component'; +import { CameraService } from '../../core/services/camera.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { DoctorService } from '../shared/services/doctor.service'; +import { MasterdataService } from '../shared/services/masterdata.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('DoctorWorklistComponent', () => { let component: DoctorWorklistComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + let mockDialog: jasmine.SpyObj; + let mockCameraService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockMasterdataService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockHttpServiceService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + let mockDoctorService: jasmine.SpyObj; + + const mockLanguageSet = { + alerts: { + info: { + imageNotFound: 'Image not found', + consulation: 'Consultation info', + confirmtoProceedFurther: 'Confirm to proceed further', + pending: 'Pending', + pendingConsult: 'Pending Consultation', + labtestDone: 'Lab test done', + }, + }, + common: { + tmReferred: 'TM Referred', + }, + }; + + beforeEach(waitForAsync(() => { + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockCameraService = jasmine.createSpyObj('CameraService', ['viewImage']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockMasterdataService = jasmine.createSpyObj('MasterdataService', [ + 'reset', + ]); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockHttpServiceService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: of(mockLanguageSet), + }); + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['reset', 'getBeneficiaryImage'] + ); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockDoctorService = jasmine.createSpyObj('DoctorService', [ + 'getDoctorWorklist', + ]); + + mockDoctorService.getDoctorWorklist.and.returnValue( + of({ statusCode: 200, data: [] }) + ); + TestBed.configureTestingModule({ declarations: [DoctorWorklistComponent], + providers: [ + { provide: MatDialog, useValue: mockDialog }, + { provide: CameraService, useValue: mockCameraService }, + { provide: Router, useValue: mockRouter }, + { provide: MasterdataService, useValue: mockMasterdataService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { + provide: SessionStorageService, + useValue: mockSessionStorageService, + }, + { provide: DoctorService, useValue: mockDoctorService }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -43,4 +127,651 @@ describe('DoctorWorklistComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should set currentRole to Doctor in session storage', () => { + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'currentRole', + 'Doctor' + ); + }); + + it('should call loadWorklist on init', () => { + expect(mockDoctorService.getDoctorWorklist).toHaveBeenCalled(); + }); + + it('should reset beneficiaryDetailsService and masterdataService', () => { + expect(mockBeneficiaryDetailsService.reset).toHaveBeenCalled(); + expect(mockMasterdataService.reset).toHaveBeenCalled(); + }); + }); + + describe('ngOnDestroy', () => { + it('should remove currentRole from sessionStorage', () => { + spyOn(sessionStorage, 'removeItem'); + component.ngOnDestroy(); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('currentRole'); + }); + }); + + describe('removeBeneficiaryDataForDoctorVisit', () => { + it('should remove multiple items from sessionStorage', () => { + spyOn(sessionStorage, 'removeItem'); + component.removeBeneficiaryDataForDoctorVisit(); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('visitCode'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith( + 'beneficiaryGender' + ); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('benFlowID'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('visitCategory'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith( + 'beneficiaryRegID' + ); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('visitID'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('beneficiaryID'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('doctorFlag'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('nurseFlag'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('pharmacist_flag'); + expect(sessionStorage.removeItem).toHaveBeenCalledWith('caseSheetTMFlag'); + }); + }); + + describe('pageChanged', () => { + it('should slice filteredBeneficiaryList based on page event', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + { id: 6 }, + ]; + component.pageChanged({ page: 2, itemsPerPage: 3 }); + expect(component.pagedList.length).toBe(3); + expect(component.pagedList as any[]).toEqual([ + { id: 4 }, + { id: 5 }, + { id: 6 }, + ]); + }); + + it('should return empty if page exceeds data length', () => { + component.filteredBeneficiaryList = [{ id: 1 }, { id: 2 }]; + component.pageChanged({ page: 3, itemsPerPage: 5 }); + expect(component.pagedList.length).toBe(0); + }); + }); + + describe('loadWorklist', () => { + it('should process worklist data when statusCode is 200', () => { + const mockData = { + statusCode: 200, + data: [ + { + beneficiaryID: '123', + doctorFlag: 1, + nurseFlag: 0, + specialist_flag: 0, + visitDate: '01-01-2023 10:00 AM', + benVisitDate: '2023-01-01T10:00:00', + }, + ], + }; + mockDoctorService.getDoctorWorklist.and.returnValue(of(mockData)); + component.loadWorklist(); + expect(component.beneficiaryList).toBeDefined(); + expect(component.filteredBeneficiaryList).toBeDefined(); + expect(component.filterTerm).toBeNull(); + }); + + it('should call confirmationService.alert on error response', () => { + const mockData = { + statusCode: 500, + errorMessage: 'Server error', + }; + mockDoctorService.getDoctorWorklist.and.returnValue(of(mockData)); + component.loadWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server error', + 'error' + ); + }); + + it('should call confirmationService.alert on http error', () => { + mockDoctorService.getDoctorWorklist.and.returnValue( + throwError('Network error') + ); + component.loadWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + + it('should not call confirmationService.alert when error is handled', () => { + mockConfirmationService.alert.calls.reset(); + mockDoctorService.getDoctorWorklist.and.returnValue( + throwError({ handled: true }) + ); + component.loadWorklist(); + expect(mockConfirmationService.alert).not.toHaveBeenCalled(); + }); + }); + + describe('loadDataToBenList', () => { + it('should set default values for missing fields', () => { + const data = [ + { + visitDate: '01-01-2023 10:00 AM', + benVisitDate: '2023-01-01T10:00:00', + }, + ]; + const result = component.loadDataToBenList(data); + expect(result[0].genderName).toBe('Not Available'); + expect(result[0].age).toBe('Not Available'); + expect(result[0].statusMessage).toBe('Not Available'); + expect(result[0].VisitCategory).toBe('Not Available'); + expect(result[0].benVisitNo).toBe('Not Available'); + expect(result[0].districtName).toBe('Not Available'); + expect(result[0].villageName).toBe('Not Available'); + expect(result[0].preferredPhoneNum).toBe('Not Available'); + expect(result[0].arrival).toBe(false); + }); + + it('should keep existing values if present', () => { + const data = [ + { + genderName: 'Male', + age: '30', + statusMessage: 'Pending', + VisitCategory: 'ANC', + benVisitNo: '1', + districtName: 'District1', + villageName: 'Village1', + preferredPhoneNum: '1234567890', + visitDate: '01-01-2023 10:00 AM', + benVisitDate: '2023-01-01T10:00:00', + }, + ]; + const result = component.loadDataToBenList(data); + expect(result[0].genderName).toBe('Male'); + expect(result[0].age).toBe('30'); + }); + }); + + describe('filterBeneficiaryList', () => { + beforeEach(() => { + component.beneficiaryList = [ + { beneficiaryID: 'BEN001', benName: 'John', genderName: 'Male' }, + { beneficiaryID: 'BEN002', benName: 'Jane', genderName: 'Female' }, + ]; + }); + + it('should reset filteredBeneficiaryList when searchTerm is empty', () => { + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toEqual( + component.beneficiaryList + ); + }); + + it('should filter by beneficiaryID', () => { + component.filterBeneficiaryList('BEN001'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe('BEN001'); + }); + + it('should filter case-insensitively', () => { + component.filterBeneficiaryList('john'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('John'); + }); + + it('should return empty for no match', () => { + component.filterBeneficiaryList('ZZZZZ'); + expect(component.filteredBeneficiaryList.length).toBe(0); + }); + }); + + describe('patientImageView', () => { + it('should call viewImage when benImage is present', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'base64imagedata' }) + ); + component.patientImageView('REG001'); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).toHaveBeenCalledWith('REG001'); + expect(mockCameraService.viewImage).toHaveBeenCalledWith( + 'base64imagedata' + ); + }); + + it('should call confirmationService.alert when benImage is not present', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue(of({})); + component.currentLanguageSet = mockLanguageSet; + component.patientImageView('REG001'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Image not found' + ); + }); + }); + + describe('loadDoctorExaminationPage', () => { + const baseBeneficiary = { + visitCode: 'VC001', + beneficiaryRegID: 'REG001', + beneficiaryID: 'BEN001', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + genderName: 'Male', + benVisitID: 'VIS001', + doctorFlag: 1, + nurseFlag: 0, + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + statusMessage: 'Pending Consultation', + }; + + it('should set visitCode in session storage', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 1, + }); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitCode', + 'VC001' + ); + }); + + it('should route to work area for statusCode 1', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 1, + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + + it('should show alert for statusCode 2', () => { + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 2, + statusMessage: 'Pending', + }); + expect(mockConfirmationService.alert).toHaveBeenCalledWith('Pending'); + }); + + it('should route to work area for statusCode 3', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 3, + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + + it('should call viewAndPrintCaseSheet for statusCode 9', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 9, + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + + it('should call viewAndPrintCaseSheet for statusCode 10', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage({ + ...baseBeneficiary, + statusCode: 10, + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + }); + + describe('viewAndPrintCaseSheet', () => { + it('should call routeToCaseSheet when user confirms', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const beneficiary = { + benFlowID: 'BF001', + VisitCategory: 'General OPD', + beneficiaryRegID: 'REG001', + benVisitID: 'VIS001', + }; + component.viewAndPrintCaseSheet(beneficiary); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'caseSheetBenFlowID', + 'BF001' + ); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + }); + + it('should not route when user cancels', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + mockRouter.navigate.calls.reset(); + const beneficiary = { + benFlowID: 'BF001', + VisitCategory: 'General OPD', + beneficiaryRegID: 'REG001', + benVisitID: 'VIS001', + }; + component.viewAndPrintCaseSheet(beneficiary); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('routeToCaseSheet', () => { + it('should set session items and navigate', () => { + const beneficiary = { + benFlowID: 'BF001', + VisitCategory: 'General OPD', + beneficiaryRegID: 'REG001', + benVisitID: 'VIS001', + }; + component.routeToCaseSheet(beneficiary); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'caseSheetBenFlowID', + 'BF001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategory', + 'General OPD' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'caseSheetBeneficiaryRegID', + 'REG001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'caseSheetVisitID', + 'VIS001' + ); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + }); + }); + + describe('routeToWorkArea', () => { + it('should call updateWorkArea when user confirms', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const beneficiary = { + beneficiaryRegID: 'REG001', + genderName: 'Male', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + benVisitID: 'VIS001', + beneficiaryID: 'BEN001', + doctorFlag: 1, + nurseFlag: 0, + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + }; + component.routeToWorkArea(beneficiary); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/attendant/doctor/patient/', + 'REG001', + ]); + }); + + it('should not navigate when user cancels', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + mockRouter.navigate.calls.reset(); + component.routeToWorkArea({ beneficiaryRegID: 'REG001' }); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('updateWorkArea', () => { + it('should navigate to doctor patient page', () => { + const beneficiary = { + beneficiaryRegID: 'REG001', + genderName: 'Male', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + benVisitID: 'VIS001', + beneficiaryID: 'BEN001', + doctorFlag: 1, + nurseFlag: 0, + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + }; + component.updateWorkArea(beneficiary); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/attendant/doctor/patient/', + 'REG001', + ]); + }); + }); + + describe('setDataForWorkArea', () => { + it('should set all session items and return true', () => { + const beneficiary = { + genderName: 'Male', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + beneficiaryRegID: 'REG001', + benVisitID: 'VIS001', + beneficiaryID: 'BEN001', + doctorFlag: 1, + nurseFlag: 0, + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + }; + const result = component.setDataForWorkArea(beneficiary); + expect(result).toBe(true); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'beneficiaryGender', + 'Male' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'benFlowID', + 'BF001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitCategory', + 'General OPD' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'beneficiaryRegID', + 'REG001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'visitID', + 'VIS001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'beneficiaryID', + 'BEN001' + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'doctorFlag', + 1 + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'nurseFlag', + 0 + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'pharmacist_flag', + 0 + ); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'phnum', + '1234567890' + ); + }); + }); + + describe('checkDoctorStatusAtTcCancelled', () => { + it('should alert when doctorFlag is 2', () => { + component.checkDoctorStatusAtTcCancelled({ + doctorFlag: 2, + nurseFlag: 0, + statusMessage: 'Pending', + }); + expect(mockConfirmationService.alert).toHaveBeenCalledWith('Pending'); + }); + + it('should alert when nurseFlag is 2', () => { + component.checkDoctorStatusAtTcCancelled({ + doctorFlag: 0, + nurseFlag: 2, + statusMessage: 'Pending', + }); + expect(mockConfirmationService.alert).toHaveBeenCalledWith('Pending'); + }); + + it('should route to work area when doctorFlag is 1', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.checkDoctorStatusAtTcCancelled({ + doctorFlag: 1, + nurseFlag: 0, + beneficiaryRegID: 'REG001', + genderName: 'Male', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + benVisitID: 'VIS001', + beneficiaryID: 'BEN001', + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + + it('should route to work area when doctorFlag is 3', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.checkDoctorStatusAtTcCancelled({ + doctorFlag: 3, + nurseFlag: 0, + beneficiaryRegID: 'REG001', + genderName: 'Male', + benFlowID: 'BF001', + VisitCategory: 'General OPD', + benVisitID: 'VIS001', + beneficiaryID: 'BEN001', + pharmacist_flag: 0, + preferredPhoneNum: '1234567890', + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + + it('should call viewAndPrintCaseSheet when doctorFlag is 9', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.checkDoctorStatusAtTcCancelled({ + doctorFlag: 9, + nurseFlag: 0, + benFlowID: 'BF001', + VisitCategory: 'General OPD', + beneficiaryRegID: 'REG001', + benVisitID: 'VIS001', + }); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + }); + }); + + describe('getVisitStatus', () => { + beforeEach(() => { + component.currentLanguageSet = mockLanguageSet; + }); + + it('should return statusCode 2 when doctorFlag is 2', () => { + const result = component.getVisitStatus({ + doctorFlag: 2, + nurseFlag: 0, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(2); + expect(result.statusMessage).toBe('Pending'); + }); + + it('should return statusCode 2 when nurseFlag is 2', () => { + const result = component.getVisitStatus({ + doctorFlag: 0, + nurseFlag: 2, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(2); + expect(result.statusMessage).toBe('Pending'); + }); + + it('should return statusCode 1 when doctorFlag is 1', () => { + const result = component.getVisitStatus({ + doctorFlag: 1, + nurseFlag: 0, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(1); + expect(result.statusMessage).toBe('Pending Consultation'); + }); + + it('should return statusCode 3 when doctorFlag is 3', () => { + const result = component.getVisitStatus({ + doctorFlag: 3, + nurseFlag: 0, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(3); + expect(result.statusMessage).toBe('Lab test done'); + }); + + it('should return statusCode 10 when specialist_flag is 100', () => { + const result = component.getVisitStatus({ + doctorFlag: 0, + nurseFlag: 0, + specialist_flag: 100, + }); + expect(result.statusCode).toBe(10); + expect(result.statusMessage).toBe('TM Referred'); + }); + + it('should return statusCode 9 when doctorFlag is 9', () => { + const result = component.getVisitStatus({ + doctorFlag: 9, + nurseFlag: 0, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(9); + expect(result.statusMessage).toBe('Consultation Done'); + }); + + it('should return statusCode 0 when no flags match', () => { + const result = component.getVisitStatus({ + doctorFlag: 0, + nurseFlag: 0, + specialist_flag: 0, + }); + expect(result.statusCode).toBe(0); + expect(result.statusMessage).toBe(''); + }); + }); + + describe('fetchLanguageResponse', () => { + it('should set currentLanguageSet', () => { + component.fetchLanguageResponse(); + expect(component.currentLanguageSet).toBeDefined(); + }); + }); + + describe('ngDoCheck', () => { + it('should call fetchLanguageResponse', () => { + spyOn(component, 'fetchLanguageResponse'); + component.ngDoCheck(); + expect(component.fetchLanguageResponse).toHaveBeenCalled(); + }); + + it('should update beneficiaryMetaData statuses when data exists', () => { + component.currentLanguageSet = mockLanguageSet; + component.beneficiaryMetaData = [ + { doctorFlag: 1, nurseFlag: 0, specialist_flag: 0 }, + ]; + component.ngDoCheck(); + expect(component.beneficiaryMetaData[0].statusCode).toBe(1); + expect(component.beneficiaryMetaData[0].statusMessage).toBe( + 'Pending Consultation' + ); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/abdominal-examination/abdominal-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/abdominal-examination/abdominal-examination.component.spec.ts index c12144b6..b93e858c 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/abdominal-examination/abdominal-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/abdominal-examination/abdominal-examination.component.spec.ts @@ -19,68 +19,109 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { of } from 'rxjs'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { CameraService } from '../../../../core/services/camera.service'; -import { CameraServiceStub } from '../../../../core/mocks/camera-service-stub'; -import { CancerUtils } from '../../../shared/utility'; import { AbdominalExaminationComponent } from './abdominal-examination.component'; +import { CameraService } from '../../../../core/services/camera.service'; +import { MaterialModule } from '../../../../core/material.module'; +import { CancerUtils } from '../../../shared/utility/cancer-utility'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; describe('AbdominalExaminationComponent', () => { let component: AbdominalExaminationComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + let camera: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [AbdominalExaminationComponent], + providers: [ + ...commonTestProviders(), + { provide: CameraService, useValue: autoSpy(CameraService) }, + ], schemas: [NO_ERRORS_SCHEMA], - providers: [{ provide: CameraService, useClass: CameraServiceStub }], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(AbdominalExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.abdominalExaminationForm = new CancerUtils( - fb - ).createAbdominalExaminationForm(); + camera = TestBed.inject(CameraService) as any; + form = new CancerUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createAbdominalExaminationForm(); + component.abdominalExaminationForm = form; fixture.detectChanges(); }); - it('should create', () => { + it('creates and sets the language', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('should not show lymph nodes', () => { - debugElement = fixture.debugElement.query(By.css('#lymphNodesTable')); - expect(debugElement).toBeFalsy(); + it('exposes lymphNodes_Enlarged and observation getters', () => { + expect(component.lymphNodes_Enlarged).toBe(form.get('lymphNodes_Enlarged')); + expect(component.observation).toBe(form.get('observation')); }); - it('should show lymph nodes when lymph nodes is enlarged', () => { - component.abdominalExaminationForm.patchValue({ - lymphNodes_Enlarged: true, - }); + it('shows lymph node detail section only when enlarged', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('#lymphNodesTable')).toBeNull(); + form.patchValue({ lymphNodes_Enlarged: true }); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lymphNodesTable')); - expect(debugElement).toBeTruthy; + expect(el.querySelector('#lymphNodesTable')).not.toBeNull(); + }); + + it('checkWithLymphNodes resets every lymph node field', () => { + form.patchValue({ + lymphNode_Inguinal_Left: true, + lymphNode_Inguinal_Right: true, + lymphNode_ExternalIliac_Left: true, + lymphNode_ExternalIliac_Right: true, + lymphNode_ParaAortic_Left: true, + lymphNode_ParaAortic_Right: true, + }); + component.checkWithLymphNodes(); + expect(form.value.lymphNode_Inguinal_Left).toBeNull(); + expect(form.value.lymphNode_Inguinal_Right).toBeNull(); + expect(form.value.lymphNode_ExternalIliac_Left).toBeNull(); + expect(form.value.lymphNode_ExternalIliac_Right).toBeNull(); + expect(form.value.lymphNode_ParaAortic_Left).toBeNull(); + expect(form.value.lymphNode_ParaAortic_Right).toBeNull(); + }); + + it('annotateImage stores the returned points with imageID 1', () => { + camera.annotate.and.returnValue(of({ markers: [1] })); + form.patchValue({ image: { old: true } }); + ( + fixture.nativeElement.querySelector( + '#annotateAbdominalImg' + ) as HTMLElement + ).click(); + expect(camera.annotate).toHaveBeenCalledWith( + 'assets/images/abdominalExamination.png', + { old: true }, + LANGUAGE_EN + ); + expect(form.value.image).toEqual({ markers: [1], imageID: 1 }); + expect(form.dirty).toBeTrue(); }); - it('should call annotateImage when image is clicked', () => { - spyOn(component, 'annotateImage'); - debugElement = fixture.debugElement.query(By.css('#annotateAbdominalImg')); - debugElement.triggerEventHandler('click', null); - expect(component.annotateImage).toHaveBeenCalled(); + it('annotateImage ignores an empty dialog result', () => { + camera.annotate.and.returnValue(of(undefined)); + component.annotateImage(); + expect(form.value.image).toBeNull(); + expect(form.dirty).toBeFalse(); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/breast-examination/breast-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/breast-examination/breast-examination.component.spec.ts index 7a876c82..4476d713 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/breast-examination/breast-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/breast-examination/breast-examination.component.spec.ts @@ -19,115 +19,138 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - +import { BreastExaminationComponent } from './breast-examination.component'; import { CameraService } from '../../../../core/services/camera.service'; import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { CameraServiceStub } from '../../../../core/mocks/camera-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { CancerUtils } from '../../../shared/utility'; -import { BreastExaminationComponent } from './breast-examination.component'; +import { MaterialModule } from '../../../../core/material.module'; +import { CancerUtils } from '../../../shared/utility/cancer-utility'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; describe('BreastExaminationComponent', () => { let component: BreastExaminationComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + let camera: any; + let benDetails$: BehaviorSubject; + let form: FormGroup; + + beforeEach(async () => { + benDetails$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [BreastExaminationComponent], - schemas: [NO_ERRORS_SCHEMA], providers: [ - { provide: CameraService, useClass: CameraServiceStub }, + ...commonTestProviders(), + { provide: CameraService, useValue: autoSpy(CameraService) }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: benDetails$ }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(BreastExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.breastExaminationForm = new CancerUtils( - fb - ).createBreastExaminationForm(); - fixture.detectChanges(); + camera = TestBed.inject(CameraService) as any; + form = new CancerUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createBreastExaminationForm(); + component.breastExaminationForm = form; }); - it('should create', () => { - expect(component).toBeTruthy(); + it('creates with language and treats unknown gender as not female', () => { + fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.female).toBeFalse(); + expect( + fixture.nativeElement.querySelector('[formControlName="everBreastFed"]') + ).toBeNull(); }); - it('should not show duration of breast feeding symptom', () => { - debugElement = fixture.debugElement.query( - By.css('#breastFeedingDurationGTE6months') - ); - expect(debugElement).toBeFalsy(); + ['FEMALE', 'transgender'].forEach(g => { + it(`shows breast-feeding questions for ${g}`, () => { + benDetails$.next({ genderName: g }); + fixture.detectChanges(); + expect(component.female).toBeTrue(); + expect( + fixture.nativeElement.querySelector('[formControlName="everBreastFed"]') + ).not.toBeNull(); + }); }); - it('should not show duration of breast feeding symptom when ever breast fed a child is true', () => { - component.everBreastFed.patchValue({ everBreastFed: true }); + it('keeps female false for male beneficiaries', () => { + benDetails$.next({ genderName: 'Male' }); fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#breastFeedingDurationGTE6months') - ); - expect(debugElement).toBeTruthy(); + expect(component.female).toBeFalse(); }); - it('should not show lump size symptom', () => { - debugElement = fixture.debugElement.query(By.css('#lumpSize')); - expect(debugElement).toBeFalsy(); + it('exposes getters', () => { + expect(component.everBreastFed).toBe(form.get('everBreastFed')); + expect(component.lumpInBreast).toBe(form.get('lumpInBreast')); }); - it('should not show lump shape symptom', () => { - debugElement = fixture.debugElement.query(By.css('#lumpShape')); - expect(debugElement).toBeFalsy(); + it('checkBreastFeed resets the duration', () => { + form.patchValue({ breastFeedingDurationGTE6months: true }); + component.checkBreastFeed(); + expect(form.value.breastFeedingDurationGTE6months).toBeNull(); }); - it('should not show lump texture symptom', () => { - debugElement = fixture.debugElement.query(By.css('#lumpTexture')); - expect(debugElement).toBeFalsy(); + it('checkLump resets lump size, shape and texture', () => { + form.patchValue({ lumpSize: 'a', lumpShape: 'b', lumpTexture: 'c' }); + component.checkLump(); + expect(form.value.lumpSize).toBeNull(); + expect(form.value.lumpShape).toBeNull(); + expect(form.value.lumpTexture).toBeNull(); }); - it('should show lump size symptom when any lump in breast is true', () => { - component.breastExaminationForm.patchValue({ lumpInBreast: true }); + it('renders lump details when a lump is present', () => { + fixture.detectChanges(); + form.patchValue({ lumpInBreast: true }); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lumpSize')); - expect(debugElement).toBeTruthy(); + expect( + fixture.nativeElement.querySelector('[formControlName="lumpSize"]') + ).not.toBeNull(); }); - it('should show lump shape symptom when any lump in breast is true', () => { - component.breastExaminationForm.patchValue({ lumpInBreast: true }); + it('annotateImage stores points with imageID 2', () => { fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lumpShape')); - expect(debugElement).toBeTruthy(); + camera.annotate.and.returnValue(of({ markers: [5] })); + ( + fixture.nativeElement.querySelector('#annotateBreastImg') as HTMLElement + ).click(); + expect(camera.annotate).toHaveBeenCalledWith( + 'assets/images/breastExamination.png', + null, + LANGUAGE_EN + ); + expect(form.value.image).toEqual({ markers: [5], imageID: 2 }); + expect(form.dirty).toBeTrue(); }); - it('should show lump texture symptom when any lump in breast is true', () => { - component.breastExaminationForm.patchValue({ lumpInBreast: true }); + it('annotateImage ignores an empty result', () => { fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lumpTexture')); - expect(debugElement).toBeTruthy(); + camera.annotate.and.returnValue(of(false)); + component.annotateImage(); + expect(form.value.image).toBeNull(); }); - it('should call annotateImage when image is clicked', () => { - spyOn(component, 'annotateImage'); - debugElement = fixture.debugElement.query(By.css('#annotateBreastImg')); - debugElement.triggerEventHandler('click', null); - expect(component.annotateImage).toHaveBeenCalled(); + it('ngOnDestroy unsubscribes a stored subscription', () => { + fixture.detectChanges(); + // getBeneficiaryDetails never assigns beneficiarySubs (current behaviour) + expect(component.beneficiarySubs).toBeUndefined(); + const sub = { unsubscribe: jasmine.createSpy('unsubscribe') }; + component.beneficiarySubs = sub; + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/cancer-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/cancer-examination.component.spec.ts index a877b1d1..83ad68fe 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/cancer-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/cancer-examination.component.spec.ts @@ -19,169 +19,452 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - import { - async, - inject, ComponentFixture, TestBed, + fakeAsync, + tick, } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { CancerUtils } from '../../shared/utility'; - -import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { DoctorService } from '../../shared/services'; -import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; - -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../core/mocks/beneficiary-details-service-stub'; -import { ConfirmationServiceStub } from '../../../core/mocks/confirmation-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { CancerExaminationComponent } from './cancer-examination.component'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { + BeneficiaryDetailsService, + ConfirmationService, +} from '../../../core/services'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; describe('CancerExaminationComponent', () => { let component: CancerExaminationComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + let doctor: any; + let confirm: any; + let benDetails$: BehaviorSubject; + let form: FormGroup; + + const session = { + visitID: 'V1', + beneficiaryRegID: 'B1', + providerServiceID: 'P1', + userName: 'doc', + beneficiaryID: 'BEN', + sessionID: 'S1', + benFlowID: 'F1', + visitCode: 'VC1', + serviceLineDetails: JSON.stringify({ vanID: 7, parkingPlaceID: 9 }), + }; + + function lymph(name: string) { + return new FormGroup({ + lymphNodeName: new FormControl(name), + size_Left: new FormControl(null), + mobility_Left: new FormControl(null), + size_Right: new FormControl(null), + mobility_Right: new FormControl(null), + vanID: new FormControl(null), + parkingPlaceID: new FormControl(null), + }); + } + + function buildForm() { + return new FormGroup({ + signsForm: new FormGroup({ + breastEnlargement: new FormControl(null), + shortnessOfBreath: new FormControl(null), + lymphNodes: new FormArray([lymph('Cervical'), lymph('Axillary')]), + }), + oralExaminationForm: new FormGroup({ + image: new FormControl(null), + preMalignantLesionTypeList: new FormControl(null), + otherLesionType: new FormControl(null), + limitedMouthOpening: new FormControl(null), + }), + breastExaminationForm: new FormGroup({ + image: new FormControl(null), + everBreastFed: new FormControl(null), + }), + abdominalExaminationForm: new FormGroup({ + image: new FormControl(null), + liver: new FormControl(null), + }), + gynecologicalExaminationForm: new FormGroup({ + image: new FormControl(null), + typeOfLesionList: new FormControl(null), + uterus_Normal: new FormControl(null), + }), + }); + } - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + benDetails$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerExaminationComponent], - schemas: [NO_ERRORS_SCHEMA], providers: [ - { provide: DoctorService, useClass: DoctorServiceStub }, - { provide: ConfirmationService, useClass: ConfirmationServiceStub }, + ...commonTestProviders({ session }), + { provide: DoctorService, useValue: autoSpy(DoctorService) }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: benDetails$ }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(CancerExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + doctor = TestBed.inject(DoctorService) as any; + confirm = TestBed.inject(ConfirmationService) as any; + form = buildForm(); + component.cancerForm = form; + spyOn(console, 'log'); + }); - fb = debugElement.injector.get(FormBuilder); - component.cancerForm = new CancerUtils(fb).createCancerExaminationForm(); + it('creates, sets language and extracts sub forms', () => { fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.signsForm).toBe(form.get('signsForm') as FormGroup); + expect(component.oralExaminationForm).toBe( + form.get('oralExaminationForm') as FormGroup + ); + expect(component.breastExaminationForm).toBe( + form.get('breastExaminationForm') as FormGroup + ); + expect(component.abdominalExaminationForm).toBe( + form.get('abdominalExaminationForm') as FormGroup + ); + expect(component.gynecologicalExaminationForm).toBe( + form.get('gynecologicalExaminationForm') as FormGroup + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('toggles breast examination with breastEnlargement', () => { + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('app-doctor-breast-examination')).toBeNull(); + form.get('signsForm.breastEnlargement')!.setValue(true); + fixture.detectChanges(); + expect(component.showBreastExamination).toBeTrue(); + expect(el.querySelector('app-doctor-breast-examination')).not.toBeNull(); + form.get('signsForm.breastEnlargement')!.setValue(false); + expect(component.showBreastExamination).toBeFalse(); }); - it('should call getBenificiaryDetails on Initialisation', () => { - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); + it('works when the form has no signsForm', () => { + component.cancerForm = new FormGroup({}); + fixture.detectChanges(); + expect(component.signsForm).toBeNull(); }); - it('should show breast examination tab for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#breastExamination')); - expect(component.female).toBe(true); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should not show breast examination tab for male beneficiary by deafult', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); + ['Female', 'Transgender'].forEach(g => { + it(`marks ${g} beneficiaries as female and shows gynec panel`, () => { + benDetails$.next({ genderName: g }); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#breastExamination')); - expect(component.female).toBe(false); - expect(debugElement).not.toBeTruthy(); - }) - )); - - it('should show breast examination tab for male beneficiary if selected breast enlargement', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - component.cancerForm.controls['signsForm'].patchValue({ - breastEnlargement: true, - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#breastExamination')); - expect(component.female).toBe(false); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should get Cancer Examination Details when mode is view', () => { - spyOn(component, 'fetchCancerExaminationDetails'); - component.mode = String('view'); - component.ngOnChanges(); - expect(component.fetchCancerExaminationDetails).toHaveBeenCalled(); + expect(component.female).toBeTrue(); + expect( + fixture.nativeElement.querySelector( + 'app-doctor-gynecological-examination' + ) + ).not.toBeNull(); + }); }); - it('should get Cancer Examination Details when mode is view', async( - inject([DoctorService], doctorService => { - spyOn(component, 'fetchCancerExaminationDetails').and.callThrough(); - spyOn(doctorService, 'getCancerExaminationDetails').and.returnValue( - Observable.of(data.cancerExaminationDetails) + it('does not mark male beneficiaries as female', () => { + benDetails$.next({ genderName: 'Male' }); + fixture.detectChanges(); + expect(component.female).toBeFalse(); + }); + + describe('getImageCoordinates', () => { + it('returns only images from dirty forms', () => { + ['oral', 'abdominal', 'gynecological', 'breast'].forEach(k => + form.get(`${k}ExaminationForm.image`)!.setValue({ id: k }) + ); + expect(component.getImageCoordinates(form)).toEqual([]); + ['oral', 'abdominal', 'gynecological', 'breast'].forEach(k => + form.get(`${k}ExaminationForm`)!.markAsDirty() ); + expect(component.getImageCoordinates(form)).toEqual([ + { id: 'oral' }, + { id: 'abdominal' }, + { id: 'gynecological' }, + { id: 'breast' }, + ]); + }); + }); + + describe('update mode', () => { + beforeEach(() => { + fixture.detectChanges(); + component.mode = 'update'; + }); - component.mode = String('view'); + it('updates and alerts success', fakeAsync(() => { + form.markAsDirty(); + doctor.updateCancerExaminationDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Saved' } }) + ); component.ngOnChanges(); + expect(doctor.updateCancerExaminationDetails).toHaveBeenCalledWith( + form, + { + beneficiaryRegID: 'B1', + benVisitID: 'V1', + providerServiceMapID: 'P1', + modifiedBy: 'doc', + beneficiaryID: 'BEN', + sessionID: 'S1', + parkingPlaceID: 9, + vanID: 7, + benFlowID: 'F1', + visitCode: 'VC1', + }, + [] + ); + expect(form.pristine).toBeTrue(); + tick(); + expect(confirm.alert).toHaveBeenCalledWith('Saved', 'success'); + })); - expect(component.fetchCancerExaminationDetails).toHaveBeenCalled(); - expect(doctorService.getCancerExaminationDetails).toHaveBeenCalled(); - }) - )); + it('alerts errorMessage on non-200', fakeAsync(() => { + doctor.updateCancerExaminationDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.ngOnChanges(); + tick(); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + })); - it('should patch examionationDetails to form when mode is view', async( - inject([DoctorService], doctorService => { - spyOn(component, 'fetchCancerExaminationDetails').and.callThrough(); - spyOn(doctorService, 'getCancerExaminationDetails').and.returnValue( - Observable.of(data.cancerExaminationDetails) + it('alerts error on failure', fakeAsync(() => { + doctor.updateCancerExaminationDetails.and.returnValue( + throwingObs('down') ); + component.ngOnChanges(); + tick(); + expect(confirm.alert).toHaveBeenCalledWith('down', 'error'); + })); + }); + + describe('view mode', () => { + beforeEach(() => { + fixture.detectChanges(); + component.mode = 'view'; + }); - component.mode = String('view'); + it('fetches details and patches the form', () => { + doctor.getCancerExaminationDetails.and.returnValue( + of({ + statusCode: 200, + data: { + breastExamination: { everBreastFed: true }, + imageCoordinates: [], + }, + }) + ); component.ngOnChanges(); + expect(doctor.getCancerExaminationDetails).toHaveBeenCalledWith( + 'B1', + 'V1' + ); + expect(form.value.breastExaminationForm.everBreastFed).toBeTrue(); + }); - expect(component.fetchCancerExaminationDetails).toHaveBeenCalled(); - expect(doctorService.getCancerExaminationDetails).toHaveBeenCalled(); - expect( - (component.cancerForm.controls.breastExaminationForm) - .controls['everBreastFed'].value - ).toEqual( - data.cancerExaminationDetails.data.breastExamination.everBreastFed + it('alerts errorMessage when data is null', fakeAsync(() => { + doctor.getCancerExaminationDetails.and.returnValue( + of({ statusCode: 200, data: null, errorMessage: 'none' }) ); - }) - )); + component.ngOnChanges(); + tick(); + expect(confirm.alert).toHaveBeenCalledWith('none', 'error'); + })); + + it('alerts error on failure', fakeAsync(() => { + doctor.getCancerExaminationDetails.and.returnValue(throwingObs('x')); + component.ngOnChanges(); + tick(); + expect(confirm.alert).toHaveBeenCalledWith('x', 'error'); + })); + }); - it('should upadte Cancer Examination Details when mode is update', () => { - spyOn(component, 'upadteCancerExaminationDetails'); - component.mode = String('update'); + it('ngOnChanges does nothing for other modes', () => { + component.mode = 'add'; component.ngOnChanges(); - expect(component.upadteCancerExaminationDetails).toHaveBeenCalled(); + expect(doctor.getCancerExaminationDetails).not.toHaveBeenCalled(); + expect(doctor.updateCancerExaminationDetails).not.toHaveBeenCalled(); + }); + + describe('filterAnnotatedImageList', () => { + it('returns the single matching image', () => { + const list = [{ imageID: 1 }, { imageID: 2 }]; + expect(component.filterAnnotatedImageList(list, 2)).toEqual({ + imageID: 2, + }); + }); + it('returns undefined when none or many match', () => { + expect(component.filterAnnotatedImageList([], 1)).toBeUndefined(); + expect( + component.filterAnnotatedImageList([{ imageID: 1 }, { imageID: 1 }], 1) + ).toBeUndefined(); + }); + }); + + describe('getMergedLymphNodeValues', () => { + it('merges api values and falls back to service line ids', () => { + const merged = component.getMergedLymphNodeValues([ + { + lymphNodeName: ' cervical ', + size_Left: null, + size_Right: null, + mobility_Left: null, + mobility_Right: null, + }, + { + lymphNodeName: 'Cervical', + size_Left: '2cm', + size_Right: null, + mobility_Left: 'Fixed', + mobility_Right: null, + vanID: 1, + parkingPlaceID: 2, + }, + ]); + expect(merged).toEqual([ + { + lymphNodeName: 'Cervical', + size_Left: '2cm', + mobility_Left: 'Fixed', + size_Right: null, + mobility_Right: null, + vanID: 1, + parkingPlaceID: 2, + }, + { + lymphNodeName: 'Axillary', + size_Left: null, + mobility_Left: null, + size_Right: null, + mobility_Right: null, + vanID: 7, + parkingPlaceID: 9, + }, + ]); + }); }); - it('should get upadte Cancer Examination Details when mode is update', async( - inject([DoctorService], doctorService => { - spyOn(component, 'upadteCancerExaminationDetails').and.callThrough(); - spyOn(doctorService, 'updateCancerExaminationDetails').and.returnValue( - Observable.of(data.updateCancerExaminationSuccessResponse) + describe('patchExaminationDetails', () => { + beforeEach(() => fixture.detectChanges()); + + it('patches every section', () => { + component.patchExaminationDetails({ + signsAndSymptoms: { shortnessOfBreath: true }, + BenCancerLymphNodeDetails: [ + { + lymphNodeName: 'Axillary', + size_Left: '1cm', + size_Right: '2cm', + mobility_Left: null, + mobility_Right: null, + }, + ], + oralExamination: { + preMalignantLesionType: 'Leukoplakia,Weird lesion,', + limitedMouthOpening: 'Yes', + }, + breastExamination: { everBreastFed: false }, + abdominalExamination: { liver: 'Normal' }, + gynecologicalExamination: { + files: [{ fileName: 'a.png' }], + typeOfLesion: 'Ulcer,Growth,', + uterus_Normal: true, + }, + imageCoordinates: [ + { imageID: 1, a: 1 }, + { imageID: 2, b: 2 }, + { imageID: 3, c: 3 }, + { imageID: 4, d: 4 }, + ], + }); + const v = form.value; + expect(v.signsForm.shortnessOfBreath).toBeTrue(); + expect(v.signsForm.lymphNodes[1].size_Left).toBe('1cm'); + expect(v.signsForm.lymphNodes[1].size_Right).toBe('2cm'); + expect(v.signsForm.lymphNodes[0].vanID).toBe(7); + expect(v.oralExaminationForm.preMalignantLesionTypeList).toEqual([ + 'Leukoplakia', + 'Weird lesion', + 'Any other lesion', + ]); + expect(v.oralExaminationForm.otherLesionType).toBe('Weird lesion'); + expect(v.oralExaminationForm.image).toEqual({ imageID: 3, c: 3 }); + expect(v.breastExaminationForm.everBreastFed).toBeFalse(); + expect(v.breastExaminationForm.image).toEqual({ imageID: 2, b: 2 }); + expect(v.abdominalExaminationForm.liver).toBe('Normal'); + expect(v.abdominalExaminationForm.image).toEqual({ imageID: 1, a: 1 }); + expect(v.gynecologicalExaminationForm.typeOfLesionList).toEqual([ + 'Ulcer', + 'Growth', + ]); + expect(v.gynecologicalExaminationForm.image).toEqual({ + imageID: 4, + d: 4, + }); + expect(doctor.gynecologicalFiles).toEqual([{ fileName: 'a.png' }]); + }); + + it('does not add other lesion when all oral lesions are known', () => { + component.patchExaminationDetails({ + oralExamination: { preMalignantLesionType: 'Melanoplakia,' }, + imageCoordinates: [], + }); + expect(form.value.oralExaminationForm.preMalignantLesionTypeList).toEqual( + ['Melanoplakia'] ); + expect(form.value.oralExaminationForm.otherLesionType).toBeNull(); + }); - component.mode = String('update'); - component.ngOnChanges(); - expect(component.upadteCancerExaminationDetails).toHaveBeenCalled(); - }) - )); + it('ignores lymph nodes that are not in the form', () => { + spyOn(component, 'getMergedLymphNodeValues').and.returnValue([ + { lymphNodeName: 'Unknown', size_Left: 'x' }, + ]); + component.patchExaminationDetails({ + signsAndSymptoms: {}, + BenCancerLymphNodeDetails: [], + }); + expect( + form.value.signsForm.lymphNodes.every((n: any) => n.size_Left === null) + ).toBeTrue(); + }); + }); + + it('unsubscribes all subscriptions on destroy', () => { + fixture.detectChanges(); + component.mode = 'update'; + component.ngOnChanges(); + component.mode = 'view'; + doctor.getCancerExaminationDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.ngOnChanges(); + const subs = [ + component.beneficiaryDetailsSubscription, + component.fetchExaminationDetailsSubs, + component.updateExaminationSubs, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + }); + + it('ngOnDestroy is safe with no subscriptions', () => { + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/gynecological-examination/gynecological-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/gynecological-examination/gynecological-examination.component.spec.ts index c7af19c2..25e1504c 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/gynecological-examination/gynecological-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/gynecological-examination/gynecological-examination.component.spec.ts @@ -19,86 +19,321 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { CameraService } from '../../../../core/services/camera.service'; -import { CameraServiceStub } from '../../../../core/mocks/camera-service-stub'; - -import { CancerUtils } from '../../../shared/utility'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs'; import { GynecologicalExaminationComponent } from './gynecological-examination.component'; +import { CameraService } from '../../../../core/services/camera.service'; +import { MaterialModule } from '../../../../core/material.module'; +import { DoctorService, NurseService } from '../../../shared/services'; +import { LabService } from 'src/app/app-modules/lab/shared/services'; +import { ConfirmationService } from 'src/app/app-modules/core/services'; +import { CancerUtils } from '../../../shared/utility/cancer-utility'; +import { ViewRadiologyUploadedFilesComponent } from 'src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; describe('GynecologicalExaminationComponent', () => { let component: GynecologicalExaminationComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + let camera: any; + let lab: any; + let nurse: any; + let confirm: any; + let dialog: any; + let form: FormGroup; + + function fileEvent(files: File[]) { + return { target: { files } }; + } - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [GynecologicalExaminationComponent], + providers: [ + ...commonTestProviders(), + { provide: CameraService, useValue: autoSpy(CameraService) }, + { provide: LabService, useValue: autoSpy(LabService) }, + { provide: NurseService, useValue: autoSpy(NurseService) }, + { provide: DoctorService, useValue: autoSpy(DoctorService) }, + ], schemas: [NO_ERRORS_SCHEMA], - providers: [{ provide: CameraService, useClass: CameraServiceStub }], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GynecologicalExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.gynecologicalExaminationForm = new CancerUtils( - fb - ).createGynecologicalExaminationForm(); + camera = TestBed.inject(CameraService) as any; + lab = TestBed.inject(LabService) as any; + nurse = TestBed.inject(NurseService) as any; + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + form = new CancerUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createGynecologicalExaminationForm(); + component.gynecologicalExaminationForm = form; fixture.detectChanges(); }); - it('should create', () => { + it('creates and loads language', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('should not show RTI/STI suffered (if reports available)', () => { - debugElement = fixture.debugElement.query(By.css('#rTIOrSTIDetail')); - expect(debugElement).toBeFalsy(); + it('exposes getters and resets RTI detail', () => { + expect(component.sufferedFromRTIOrSTI).toBe( + form.get('sufferedFromRTIOrSTI') + ); + expect(component.observation).toBe(form.get('observation')); + form.patchValue({ rTIOrSTIDetail: 'x' }); + component.checkWithRTIOrSTI(); + expect(form.value.rTIOrSTIDetail).toBeNull(); }); - it('should not show select file', () => { - debugElement = fixture.debugElement.query(By.css('#files')); - expect(debugElement).toBeFalsy(); + it('renders the file upload section when RTI/STI is reported', () => { + form.patchValue({ sufferedFromRTIOrSTI: true }); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('#files')).not.toBeNull(); }); - it('should show RTI/STI suffered (if reports available) when suffered from any RTI/STI is true', () => { - component.gynecologicalExaminationForm.patchValue({ - sufferedFromRTIOrSTI: true, + describe('uploadFile', () => { + it('ignores empty file lists', () => { + component.uploadFile(fileEvent([])); + expect(component.file).toBeUndefined(); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + + it('rejects files without a name', () => { + component.uploadFile(fileEvent([new File(['a'], '.png')])); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.invalidFileName, + 'error' + ); + }); + + it('rejects invalid extensions', () => { + component.uploadFile(fileEvent([new File(['a'], 'virus.exe')])); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.invalidFileExtensionSupportedFileFormats, + 'error' + ); + }); + + it('rejects files larger than max size', () => { + component.maxFileSize = 0; + component.uploadFile(fileEvent([new File(['abc'], 'big.pdf')])); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.fileSizeShouldNotExceed + ' 0 ' + LANGUAGE_EN.mb, + 'error' + ); + }); + + it('reads a valid file and emits fileData', done => { + component.fileDataChange.subscribe((data: any[]) => { + expect(data.length).toBe(1); + expect(data[0]).toEqual({ + fileName: 'report.png', + fileExtension: '.png', + fileContent: btoa('hello'), + isUploaded: false, + }); + done(); + }); + component.uploadFile(fileEvent([new File(['hello'], 'report.png')])); }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#rTIOrSTIDetail')); - expect(debugElement).toBeTruthy(); }); - it('should show select file when suffered from any RTI/STI is true', () => { - component.gynecologicalExaminationForm.patchValue({ - sufferedFromRTIOrSTI: true, + describe('checkForDuplicateUpload', () => { + beforeEach(() => spyOn(component, 'saveUploadDetails')); + + it('alerts when fileData is undefined', () => { + component.fileData = undefined as any; + component.checkForDuplicateUpload(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pleaseselectfiletoupload, + 'info' + ); + }); + + it('saves fileObj when savedFileData is undefined', () => { + component.fileData = []; + component.savedFileData = undefined; + component.fileObj = [{ a: 1 }]; + component.checkForDuplicateUpload(); + expect(component.saveUploadDetails).toHaveBeenCalledWith([{ a: 1 }]); + }); + + it('alerts when there are no new files', () => { + component.fileData = []; + component.savedFileData = []; + component.checkForDuplicateUpload(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pleaseselectfiletoupload, + 'info' + ); + }); + + it('saves only files not matching saved upload state', () => { + const f = { fileName: 'a', isUploaded: false }; + component.fileData = [f]; + component.savedFileData = []; + component.checkForDuplicateUpload(); + expect(component.fileObj).toEqual([f]); + expect(component.saveUploadDetails).toHaveBeenCalledWith([f]); + }); + + it('alerts when every new file matches a saved one', () => { + component.fileData = [ + { fileName: 'a', isUploaded: true }, + { fileName: 'b', isUploaded: true }, + ]; + component.savedFileData = [{ fileName: 'a', isUploaded: true }]; + component.checkForDuplicateUpload(); + expect(component.saveUploadDetails).not.toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pleaseselectfiletoupload, + 'info' + ); + }); + }); + + describe('saveUploadDetails', () => { + it('stores saved files and patches file IDs on success', () => { + const obj = [{ fileName: 'a', isUploaded: false }]; + component.fileObj = obj; + component.viewFiles = [{ filePath: 'old/path' }]; + lab.saveFile.and.returnValue( + of({ statusCode: 200, data: [{ filePath: 'new/path' }] }) + ); + component.saveUploadDetails(obj); + expect(lab.saveFile).toHaveBeenCalledWith(obj); + expect(component.savedFileData).toEqual([ + { filePath: 'new/path', isUploaded: true }, + ]); + expect(obj[0].isUploaded).toBeTrue(); + expect(form.value.fileIDs).toEqual(['new/path', 'old/path']); + expect(form.dirty).toBeTrue(); + expect(confirm.alert).toHaveBeenCalledWith( + 'File Uploaded successfully', + 'success' + ); + expect(nurse.fileData).toBeNull(); + }); + + it('does nothing with a non-200 response', () => { + lab.saveFile.and.returnValue(of({ statusCode: 500 })); + component.viewFiles = []; + component.saveUploadDetails([]); + expect(component.savedFileData).toEqual([]); + expect(form.value.fileIDs).toEqual([]); + }); + + it('alerts on error', () => { + lab.saveFile.and.returnValue(throwingObs({ errorMessage: 'oops' })); + component.saveUploadDetails([]); + expect(confirm.alert).toHaveBeenCalledWith('oops', 'err'); + }); + + it('patches empty array when fileIDs is null', () => { + lab.saveFile.and.returnValue(of({ statusCode: 500 })); + component.viewFiles = null as any; + component.fileIDs = null; + component.saveUploadDetails([]); + expect(form.value.fileIDs).toEqual([]); }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#files')); - expect(debugElement).toBeTruthy(); }); - it('should call annotateImage when image is clicked', () => { - spyOn(component, 'annotateImage'); - debugElement = fixture.debugElement.query( - By.css('#annotateGynecologicalImg') + it('onLoadFileCallback reads the target result', () => { + expect(() => + component.onLoadFileCallback({ currentTarget: { result: 'x' } }) + ).not.toThrow(); + }); + + it('annotateImage stores points with imageID 4', () => { + camera.annotate.and.returnValue(of({ markers: [] })); + ( + fixture.nativeElement.querySelector( + '#annotateGynecologicalImg' + ) as HTMLElement + ).click(); + expect(camera.annotate).toHaveBeenCalledWith( + 'assets/images/gynecologicalExamination.png', + null, + LANGUAGE_EN ); - debugElement.triggerEventHandler('click', null); - expect(component.annotateImage).toHaveBeenCalled(); + expect(form.value.image).toEqual({ markers: [], imageID: 4 }); + expect(form.dirty).toBeTrue(); + }); + + it('annotateImage ignores empty result', () => { + camera.annotate.and.returnValue(of(null)); + component.annotateImage(); + expect(form.value.image).toBeNull(); + }); + + it('removeFile drops the file and updates fileIDs', () => { + component.fileData = [{ fileName: 'a' }, { fileName: 'b' }]; + component.removeFile(0); + expect(component.fileData).toEqual([{ fileName: 'b' }]); + // updateFormFileIDs maps `name`, which file objects do not have + expect(form.value.fileIDs).toEqual([undefined]); + }); + + it('checkExtension validates allowed extensions', () => { + expect(component.checkExtension(new File([''], 'a.PDF'))).toBeTrue(); + expect(component.checkExtension(new File([''], 'a.docx'))).toBeTrue(); + expect(component.checkExtension(new File([''], 'a.gif'))).toBeFalse(); + }); + + it('showError alerts the message', () => { + component.showError('bad'); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + }); + + it('triggerLog clicks the file input only for real clicks', () => { + form.patchValue({ sufferedFromRTIOrSTI: true }); + fixture.detectChanges(); + const input = fixture.nativeElement.querySelector('#files') as HTMLElement; + const clickSpy = spyOn(input, 'click'); + component.triggerLog({ clientX: 0 }); + expect(clickSpy).not.toHaveBeenCalled(); + component.triggerLog({ clientX: 10 }); + expect(clickSpy).toHaveBeenCalled(); + }); + + describe('viewNurseSelectedFiles', () => { + it('opens the viewer and downloads the chosen file', () => { + const file = { fileName: 'r.pdf', filePath: 'p' }; + component.viewFiles = [file]; + dialog.open.and.returnValue(createDialogRefMock(file)); + lab.viewFileContent.and.returnValue(of(new Blob(['x']))); + const clickSpy = spyOn(HTMLAnchorElement.prototype, 'click'); + spyOn(window.URL, 'createObjectURL').and.returnValue('blob:x'); + component.viewNurseSelectedFiles(); + expect(dialog.open).toHaveBeenCalledWith( + ViewRadiologyUploadedFilesComponent, + jasmine.objectContaining({ + width: '40%', + data: jasmine.objectContaining({ filesDetails: [file] }), + }) + ); + expect(lab.viewFileContent).toHaveBeenCalledWith(file); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('does nothing when the viewer closes without a selection', () => { + dialog.open.and.returnValue(createDialogRefMock(undefined)); + component.viewNurseSelectedFiles(); + expect(lab.viewFileContent).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/oral-examination/oral-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/oral-examination/oral-examination.component.spec.ts index fd07e04f..f950fb0b 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/oral-examination/oral-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/oral-examination/oral-examination.component.spec.ts @@ -19,87 +19,105 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { CameraService } from '../../../../core/services/camera.service'; -import { CameraServiceStub } from '../../../../core/mocks/camera-service-stub'; - -import { CancerUtils } from '../../../shared/utility'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { of } from 'rxjs'; import { OralExaminationComponent } from './oral-examination.component'; +import { CameraService } from '../../../../core/services/camera.service'; +import { MaterialModule } from '../../../../core/material.module'; +import { CancerUtils } from '../../../shared/utility/cancer-utility'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; describe('OralExaminationComponent', () => { let component: OralExaminationComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + let camera: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [OralExaminationComponent], + providers: [ + ...commonTestProviders(), + { provide: CameraService, useValue: autoSpy(CameraService) }, + ], schemas: [NO_ERRORS_SCHEMA], - providers: [{ provide: CameraService, useClass: CameraServiceStub }], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(OralExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.oralExaminationForm = new CancerUtils( - fb - ).createOralExaminationForm(); + camera = TestBed.inject(CameraService) as any; + form = new CancerUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createOralExaminationForm(); + component.oralExaminationForm = form; fixture.detectChanges(); }); - it('should create', () => { + it('creates and loads language on ngDoCheck', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('should not show type of premalignant lesion', () => { - debugElement = fixture.debugElement.query( - By.css('#preMalignantLesionTypeList') + it('exposes form control getters', () => { + expect(component.premalignantLesions).toBe(form.get('premalignantLesions')); + expect(component.preMalignantLesionType).toBe( + form.get('preMalignantLesionType') ); - expect(debugElement).toBeFalsy(); + expect(component.observation).toBe(form.get('observation')); }); - it('should not show other lesion', () => { - debugElement = fixture.debugElement.query(By.css('#otherLesionType')); - expect(debugElement).toBeFalsy(); + it('shows the other lesion input when "Any other lesion" is selected', () => { + form.patchValue({ premalignantLesions: true }); + form.patchValue({ preMalignantLesionTypeList: ['Any other lesion'] }); + fixture.detectChanges(); + expect(component.showOther).toBeTrue(); + expect( + fixture.nativeElement.querySelector('[formControlName="otherLesionType"]') + ).not.toBeNull(); }); - it('should show other lesion when preMalignantLesionTypeList contians Any other lesion', () => { - component.oralExaminationForm.patchValue({ premalignantLesions: true }); - component.oralExaminationForm.patchValue({ - preMalignantLesionTypeList: ['Any other lesion'], - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#otherLesionType')); - expect(debugElement).toBeTruthy(); + it('hides and clears the other lesion when it is deselected', () => { + form.patchValue({ preMalignantLesionTypeList: ['Any other lesion'] }); + form.patchValue({ otherLesionType: 'Custom' }); + form.patchValue({ preMalignantLesionTypeList: ['Leukoplakia'] }); + expect(component.showOther).toBeFalse(); + expect(form.value.otherLesionType).toBeNull(); }); - it('should show type of premalignant lesion when premalignant lesions is true', () => { - component.oralExaminationForm.patchValue({ premalignantLesions: true }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#preMalignantLesionTypeList') + it('clears the other lesion when list becomes null', () => { + form.patchValue({ otherLesionType: 'Custom' }); + component.checkWithPremalignantLesion(); + expect(form.value.preMalignantLesionTypeList).toBeNull(); + expect(form.value.otherLesionType).toBeNull(); + }); + + it('annotateImage stores points with imageID 3', () => { + camera.annotate.and.returnValue(of({ markers: [] })); + ( + fixture.nativeElement.querySelector('#annotateOralImg') as HTMLElement + ).click(); + expect(camera.annotate).toHaveBeenCalledWith( + 'assets/images/oralExamination.png', + null, + LANGUAGE_EN ); - expect(debugElement).toBeTruthy(); + expect(form.value.image).toEqual({ markers: [], imageID: 3 }); + expect(form.dirty).toBeTrue(); }); - it('should call annotateImage when image is clicked', () => { - spyOn(component, 'annotateImage'); - debugElement = fixture.debugElement.query(By.css('#annotateOralImg')); - debugElement.triggerEventHandler('click', null); - expect(component.annotateImage).toHaveBeenCalled(); + it('annotateImage ignores an empty result', () => { + camera.annotate.and.returnValue(of(null)); + component.annotateImage(); + expect(form.value.image).toBeNull(); + expect(form.dirty).toBeFalse(); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/cancer-examination/signs-and-symptoms/signs-and-symptoms.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/cancer-examination/signs-and-symptoms/signs-and-symptoms.component.spec.ts index 9c0103e2..9cf00610 100644 --- a/src/app/app-modules/nurse-doctor/examination/cancer-examination/signs-and-symptoms/signs-and-symptoms.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/cancer-examination/signs-and-symptoms/signs-and-symptoms.component.spec.ts @@ -19,191 +19,139 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; - -import { CancerUtils } from '../../../shared/utility'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; import { SignsAndSymptomsComponent } from './signs-and-symptoms.component'; +import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; +import { MaterialModule } from '../../../../core/material.module'; +import { CancerUtils } from '../../../shared/utility/cancer-utility'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('SignsAndSymptomsComponent', () => { let component: SignsAndSymptomsComponent; let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + let benDetails$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + benDetails$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [SignsAndSymptomsComponent], - schemas: [NO_ERRORS_SCHEMA], providers: [ + ...commonTestProviders(), { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: benDetails$ }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(SignsAndSymptomsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + form = new CancerUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createSignsForm(); + component.signsForm = form; + }); + + it('creates, loads language and fills the lymph node table source', () => { + fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.dataSource.data.length).toBe(9); + expect(component.female).toBeFalse(); + expect(component.female18).toBeFalse(); + expect(component.female30).toBeFalse(); + }); + + it('sets female flags for a 35 year old female', () => { + benDetails$.next({ genderName: 'Female', ageVal: 35 }); + fixture.detectChanges(); + expect(component.female).toBeTrue(); + expect(component.female18).toBeTrue(); + expect(component.female30).toBeTrue(); + expect( + fixture.nativeElement.querySelectorAll('mat-radio-group').length + ).toBeGreaterThan(5); + }); - fb = debugElement.injector.get(FormBuilder); - component.signsForm = new CancerUtils(fb).createSignsForm(); + it('sets only female/female18 for a 20 year old female', () => { + benDetails$.next({ genderName: 'female', ageVal: 20 }); fixture.detectChanges(); + expect(component.female).toBeTrue(); + expect(component.female18).toBeTrue(); + expect(component.female30).toBeFalse(); + }); + + it('treats transgender as female without age flags', () => { + benDetails$.next({ genderName: 'Transgender', ageVal: 40 }); + fixture.detectChanges(); + expect(component.female).toBeTrue(); + expect(component.female18).toBeFalse(); + expect(component.female30).toBeFalse(); + }); + + it('resets female flags when gender changes to male', () => { + benDetails$.next({ genderName: 'Female', ageVal: 40 }); + fixture.detectChanges(); + benDetails$.next({ genderName: 'Male', ageVal: 40 }); + expect(component.female).toBeFalse(); + expect(component.female30).toBeFalse(); + }); + + it('exposes getters', () => { + expect(component.observation).toBe(form.get('observation')); + form.patchValue({ lymphNode_Enlarged: true }); + expect(component.lymphNode_Enlarged).toBeTrue(); + expect(component.lymphNodes.length).toBe(9); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('getLymphNodes returns controls or null', () => { + expect(component.getLymphNodes()!.length).toBe(9); + component.signsForm = new FormGroup({}); + expect(component.getLymphNodes()).toBeNull(); }); - it('should call getBenificiaryDetails on Initialisation', () => { - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); + it('checkLymph resets lymph node values when not enlarged', () => { + const nodes = form.get('lymphNodes')!; + nodes.patchValue([{ size_Left: '<3 cm' }]); + component.checkLymph(false); + expect(nodes.value[0].size_Left).toBeNull(); + expect(component.dataSource.data.length).toBe(9); }); - it('should hide lumpInTheBreast symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lumpInTheBreast')); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should hide bloodStainedDischargeFromNipple symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#bloodStainedDischargeFromNipple') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should hide changeInShapeAndSizeOfBreasts symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#changeInShapeAndSizeOfBreasts') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should hide vaginalBleedingBetweenPeriods symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#vaginalBleedingBetweenPeriods') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should hide vaginalBleedingAfterMenopause symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#vaginalBleedingAfterMenopause') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should hide foulSmellingVaginalDischarge symptom for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#foulSmellingVaginalDischarge') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should show lumpInTheBreast symptom for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lumpInTheBreast')); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should show bloodStainedDischargeFromNipple symptom for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#bloodStainedDischargeFromNipple') - ); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should show changeInShapeAndSizeOfBreasts symptom for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#changeInShapeAndSizeOfBreasts') - ); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should show vaginalBleedingBetweenPeriods symptom for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#vaginalBleedingBetweenPeriods') - ); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should not show vaginalBleedingAfterMenopause symptom for female beneficiary less than 30 years', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#vaginalBleedingAfterMenopause') - ); - expect(debugElement).toBeFalsy(); - }) - )); - - it('should show foulSmellingVaginalDischarge symptom for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#foulSmellingVaginalDischarge') - ); - expect(debugElement).toBeTruthy(); - }) - )); + it('checkLymph keeps values when enlarged', () => { + const nodes = form.get('lymphNodes')!; + nodes.patchValue([{ size_Left: '<3 cm' }]); + component.checkLymph(true); + expect(nodes.value[0].size_Left).toBe('<3 cm'); + }); + + it('checkLymph sets empty data source without lymphNodes array', () => { + component.dataSource.data = [1]; + component.signsForm = new FormGroup({}); + component.checkLymph(true); + // MatTableDataSource normalises the null it is given to [] + expect(component.dataSource.data).toEqual([]); + }); + + it('unsubscribes on destroy', () => { + fixture.detectChanges(); + const sub = component.beneficiaryDetailsSubs; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('ngOnDestroy is safe before init', () => { + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/examination.component.spec.ts index 37f02ee1..3bfbc4a5 100644 --- a/src/app/app-modules/nurse-doctor/examination/examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/examination.component.spec.ts @@ -19,144 +19,87 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../core/material.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; import { ExaminationComponent } from './examination.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('ExaminationComponent', () => { let component: ExaminationComponent; let fixture: ComponentFixture; - let debugElement; - let fb; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ExaminationComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(ExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fixture.detectChanges(); - }); - - it('should create ExaminationComponent', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize ExaminationComponent', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + component.patientExaminationDataForm = new FormGroup({}); + component.examinationMode = 'add'; }); - it('should excute on changes', () => { - component.ngOnChanges(); - expect(component).toBeTruthy(); - }); - - it('Should show general examination while selecting visitCategory ANC', () => { - component.ngOnInit(); - component.visitCategory = 'ANC'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - }); - - it('Should show general examination while selecting visitCategory ANC', () => { - component.ngOnInit(); - component.visitCategory = 'General OPD'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - }); - - it('Should show cancer examination', () => { - component.ngOnInit(); - component.visitCategory = 'Cancer Screening'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); - }); - - it('Should show cancer examination', () => { - component.ngOnInit(); - component.visitCategory = 'Cancer Screening'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); + it('creates and loads the language set on init', () => { fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('cancer-examination')); - expect(de.nativeElement).toBeTruthy(); + expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.languageComponent).toBeDefined(); }); - it('Should show General examination', () => { - component.ngOnInit(); - component.visitCategory = 'General OPD'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-general-opd-examination') - ); - expect(de.nativeElement).toBeTruthy(); + it('refreshes the language set on ngDoCheck', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('Should show General examination', () => { - component.ngOnInit(); - component.visitCategory = 'ANC'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-general-opd-examination') - ); - expect(de.nativeElement).toBeTruthy(); + ['General OPD', 'ANC', 'PNC'].forEach(cat => { + it(`shows the general OPD examination for ${cat}`, () => { + component.visitCategory = cat; + component.ngOnChanges(); + fixture.detectChanges(); + expect(component.showGeneralOPD).toBeTrue(); + expect(component.showCancer).toBeFalse(); + const el: HTMLElement = fixture.nativeElement; + expect( + el.querySelector('app-nurse-general-opd-examination') + ).not.toBeNull(); + expect(el.querySelector('app-cancer-examination')).toBeNull(); + }); }); - it('Should not show general examination', () => { - component.ngOnInit(); + it('shows the cancer examination for Cancer Screening', () => { component.visitCategory = 'Cancer Screening'; component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-general-opd-examination') - ); - expect(de).not.toBeTruthy(); + expect(component.showGeneralOPD).toBeFalse(); + expect(component.showCancer).toBeTrue(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('app-cancer-examination')).not.toBeNull(); + expect(el.querySelector('app-nurse-general-opd-examination')).toBeNull(); }); - it('Should not show cancer examination', () => { - component.ngOnInit(); - component.visitCategory = 'General OPD'; + it('hides both examinations for other categories', () => { + component.visitCategory = 'NCD Screening'; component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('cancer-examination')); - expect(de).not.toBeTruthy(); + expect(component.showGeneralOPD).toBeFalse(); + expect(component.showCancer).toBeFalse(); }); - it('Should not show cancer examination', () => { - component.ngOnInit(); - component.visitCategory = 'ANC'; + it('leaves flags untouched when no visit category is set', () => { + component.showGeneralOPD = true; + component.showCancer = true; + component.visitCategory = ''; component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('cancer-examination')); - expect(de).not.toBeTruthy(); + expect(component.showGeneralOPD).toBeTrue(); + expect(component.showCancer).toBeTrue(); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-examination/general-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-examination/general-examination.component.spec.ts index 23922f42..3fddf716 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-examination/general-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-examination/general-examination.component.spec.ts @@ -19,220 +19,132 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { GeneralExaminationComponent } from './general-examination.component'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService } from '../../../shared/services'; - -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { MaterialModule } from '../../../../core/material.module'; +import { GeneralUtils } from '../../../shared/utility/general-utility'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('GeneralExaminationComponent', () => { let component: GeneralExaminationComponent; let fixture: ComponentFixture; - let doctorService: DoctorService; - let confirmationService: ConfirmationService; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let session: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [GeneralExaminationComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], - providers: [ - ConfirmationService, - { provide: DoctorService, useClass: DoctorServiceStub }, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GeneralExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.generalExaminationForm = new GeneralUtils( - fb - ).createGeneralExaminationForm(); - window.console.log = () => {}; - fixture.detectChanges(); - }); - - it('should create GeneralExaminationComponent', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize GeneralExaminationComponent', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); - }); - - it('should check value of danger sign and make null to dependent fields', async(() => { - spyOn(component, 'checkWithDangerSign').and.callThrough(); - component.generalExaminationForm.patchValue({ dangerSigns: 'Yes' }); - component.generalExaminationForm.patchValue({ - typeOfDangerSigns: ['1', '2'], - }); - debugElement = fixture.debugElement.query(By.css('#dangerSigns')); - debugElement.nativeElement.value = 'Yes'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.dangerSigns).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWithDangerSign).toHaveBeenCalled(); - expect(component.typeOfDangerSigns).toEqual(null); - })); - - it('should check value of lymphadenopathy and make null to dependent fields', async(() => { - spyOn(component, 'checkWithLymphadenopathy').and.callThrough(); - component.generalExaminationForm.patchValue({ lymphadenopathy: 'Present' }); - component.generalExaminationForm.patchValue({ - lymphnodesInvolved: 'Cervical LN', - }); - component.generalExaminationForm.patchValue({ - typeOfLymphadenopathy: 'Soft', - }); - debugElement = fixture.debugElement.query(By.css('#lymphadenopathy')); - debugElement.nativeElement.value = 'Present'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.lymphadenopathy).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWithLymphadenopathy).toHaveBeenCalled(); - expect(component.lymphnodesInvolved).toEqual(null); - expect(component.typeOfLymphadenopathy).toEqual(null); - })); - - it('should check value of edema and make null to dependent fields', async(() => { - spyOn(component, 'checkWithEdema').and.callThrough(); - component.generalExaminationForm.patchValue({ edema: 'Present' }); - component.generalExaminationForm.patchValue({ extentOfEdema: 'Foot' }); - component.generalExaminationForm.patchValue({ edemaType: 'Pitting' }); - debugElement = fixture.debugElement.query(By.css('#edema')); - debugElement.nativeElement.value = 'Present'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.edema).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWithEdema).toHaveBeenCalled(); - expect(component.extentOfEdema).toEqual(null); - expect(component.edemaType).toEqual(null); - })); - - it('should disable type of danger sign when dangerSigns is No', () => { - component.generalExaminationForm.patchValue({ dangerSigns: 'No' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#typeOfDangerSigns')); - expect(debugElement).not.toBeTruthy(); + session = TestBed.inject(SessionStorageService) as any; + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createGeneralExaminationForm(); + component.generalExaminationDataForm = form; }); - it('should able type of danger sign when dangerSigns is Yes', () => { - component.generalExaminationForm.patchValue({ dangerSigns: 'Yes' }); + it('creates and loads language', () => { fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#typeOfDangerSigns')); - expect(debugElement).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.selectDangerSigns.length).toBe(13); }); - it('should disable type of danger sign when dangerSigns is No', () => { - component.generalExaminationForm.patchValue({ dangerSigns: 'No' }); + it('shows ANC-only fields when visiCategoryANC is ANC', () => { + session.setItem('visiCategoryANC', 'ANC'); + component.ngOnChanges(); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#typeOfDangerSigns')); - expect(debugElement).not.toBeTruthy(); + expect(component.visitCategory).toBe('ANC'); + expect(component.hideForANCAndQC).toBeTrue(); + expect( + fixture.nativeElement.querySelector('[formControlName="quickening"]') + ).not.toBeNull(); }); - it('should able lymphnodesInvolved when lymphadenopathy is Present ', () => { - component.generalExaminationForm.patchValue({ lymphadenopathy: 'Present' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lymphnodesInvolved')); - expect(debugElement).toBeTruthy(); + it('shows ANC-only fields when visitCategory is ANC', () => { + session.setItem('visitCategory', 'ANC'); + component.ngOnChanges(); + expect(component.hideForANCAndQC).toBeTrue(); }); - it('should disable type of danger sign when dangerSigns is Absent', () => { - component.generalExaminationForm.patchValue({ lymphadenopathy: 'Absent' }); + it('hides ANC-only fields for other visits', () => { + session.setItem('visitCategory', 'General OPD'); + component.ngOnChanges(); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lymphnodesInvolved')); - expect(debugElement).not.toBeTruthy(); + expect(component.hideForANCAndQC).toBeFalse(); + expect( + fixture.nativeElement.querySelector('[formControlName="quickening"]') + ).toBeNull(); }); - it('should able typeOfLymphadenopathy when lymphadenopathy is Present ', () => { - component.generalExaminationForm.patchValue({ lymphadenopathy: 'Present' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#typeOfLymphadenopathy')); - expect(debugElement).toBeTruthy(); + it('checkWithDangerSign clears danger sign types', () => { + form.patchValue({ typeOfDangerSigns: ['Stridor'] }); + component.checkWithDangerSign(); + expect(form.value.typeOfDangerSigns).toBeNull(); }); - it('should disable typeOfLymphadenopathy when lymphadenopathy is is Absent', () => { - component.generalExaminationForm.patchValue({ lymphadenopathy: 'Absent' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#typeOfLymphadenopathy')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should able edemaType when edema is Present ', () => { - component.generalExaminationForm.patchValue({ edema: 'Present' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#edemaType')); - expect(debugElement).toBeTruthy(); + it('checkWithLymphadenopathy clears lymph node fields', () => { + form.patchValue({ + lymphnodesInvolved: ['a'], + typeOfLymphadenopathy: ['b'], + }); + component.checkWithLymphadenopathy(); + expect(form.value.lymphnodesInvolved).toBeNull(); + expect(form.value.typeOfLymphadenopathy).toBeNull(); }); - it('should able extentOfEdema when edema is Present ', () => { - component.generalExaminationForm.patchValue({ edema: 'Present' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#extentOfEdema')); - expect(debugElement).toBeTruthy(); + it('checkWithEdema clears edema fields', () => { + form.patchValue({ extentOfEdema: ['Foot'], edemaType: 'Pitting' }); + component.checkWithEdema(); + expect(form.value.extentOfEdema).toBeNull(); + expect(form.value.edemaType).toBeNull(); }); - it('should able edemaType when edema is Absent ', () => { - component.generalExaminationForm.patchValue({ edema: 'Absent' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#edemaType')); - expect(debugElement).not.toBeTruthy(); + it('getters return control values', () => { + form.patchValue({ + dangerSigns: 'Yes', + edema: 'Yes', + lymphadenopathy: 'No', + typeOfDangerSigns: ['Grunt'], + extentOfEdema: ['Leg'], + lymphnodesInvolved: ['Axillary LN'], + typeOfLymphadenopathy: ['Soft'], + edemaType: 'Non Pitting', + quickening: 'Yes', + foetalMovements: 'Normal', + }); + expect(component.dangerSigns).toBe('Yes'); + expect(component.edema).toBe('Yes'); + expect(component.lymphadenopathy).toBe('No'); + expect(component.typeOfDangerSigns).toEqual(['Grunt']); + expect(component.extentOfEdema).toEqual(['Leg']); + expect(component.lymphnodesInvolved).toEqual(['Axillary LN']); + expect(component.typeOfLymphadenopathy).toEqual(['Soft']); + expect(component.edemaType).toBe('Non Pitting'); + expect(component.quickening).toBe('Yes'); + expect(component.foetalmovements).toBe('Normal'); }); - it('should able extentOfEdema when edema is Absent ', () => { - component.generalExaminationForm.patchValue({ edema: 'Absent' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#extentOfEdema')); - expect(debugElement).not.toBeTruthy(); + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Pallor'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Pallor', + 'General Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-opd-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-opd-examination.component.spec.ts index 4bf560c8..b500ad2d 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-opd-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/general-opd-examination.component.spec.ts @@ -19,128 +19,292 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { of } from 'rxjs'; import { GeneralOpdExaminationComponent } from './general-opd-examination.component'; -import { GeneralUtils } from '../../shared/utility'; - -import { ConfirmationService } from '../../../core/services/confirmation.service'; import { DoctorService } from '../../shared/services'; - -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; describe('GeneralOpdExaminationComponent', () => { let component: GeneralOpdExaminationComponent; let fixture: ComponentFixture; - let doctorService: DoctorService; - let confirmationService: ConfirmationService; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ + let doctor: any; + let confirm: any; + let form: FormGroup; + + const session = { + visitID: 'V1', + beneficiaryRegID: 'B1', + providerServiceID: 'P1', + userName: 'nurse', + beneficiaryID: 'BEN', + sessionID: 'S1', + benFlowID: 'F1', + visitCode: 'VC1', + serviceLineDetails: JSON.stringify({ vanID: 7, parkingPlaceID: 9 }), + }; + + const genControls = [ + 'typeOfDangerSigns', + 'lymphnodesInvolved', + 'typeOfLymphadenopathy', + 'extentOfEdema', + 'edemaType', + ]; + + function buildForm(required = false) { + const gen: any = {}; + genControls.forEach( + c => (gen[c] = new FormControl(null, required ? Validators.required : [])) + ); + gen.pallor = new FormControl(null); + return new FormGroup({ + generalExaminationForm: new FormGroup(gen), + headToToeExaminationForm: new FormGroup({ head: new FormControl() }), + systemicExaminationForm: new FormGroup({ + cardioVascularSystemForm: new FormGroup({ x: new FormControl() }), + gastroIntestinalSystemForm: new FormGroup({ g: new FormControl() }), + obstetricExaminationForANCForm: new FormGroup({ + o: new FormControl(), + }), + }), + }); + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralOpdExaminationComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], providers: [ - ConfirmationService, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ session }), + { provide: DoctorService, useValue: autoSpy(DoctorService) }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GeneralOpdExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + doctor = TestBed.inject(DoctorService) as any; + confirm = TestBed.inject(ConfirmationService) as any; + form = buildForm(); + component.patientExaminationForm = form; + component.visitCategory = 'General OPD'; + }); - fb = debugElement.injector.get(FormBuilder); - component.patientExaminationForm = new GeneralUtils( - fb - ).createPatientExaminationForm(); + it('creates, loads language and extracts sub forms on init', () => { fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.generalExaminationForm).toBe( + form.get('generalExaminationForm') as FormGroup + ); + expect(component.headToToeExaminationForm).toBe( + form.get('headToToeExaminationForm') as FormGroup + ); + expect(component.systemicExaminationForm).toBe( + form.get('systemicExaminationForm') as FormGroup + ); }); - it('should create GeneralOpdExaminationComponent', () => { - expect(component).toBeTruthy(); + it('renders the systemic panel except for NCD Care', () => { + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('app-nurse-general-examination')).not.toBeNull(); + expect( + el.querySelector('app-nurse-head-to-toe-examination') + ).not.toBeNull(); + expect(el.querySelector('app-nurse-systemic-examination')).not.toBeNull(); + component.visitCategory = 'NCD Care'; + fixture.detectChanges(); + expect(el.querySelector('app-nurse-systemic-examination')).toBeNull(); }); - it('should initialize GeneralOpdExaminationComponent', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('ngOnChanges in add mode only reloads sub forms', () => { + component.mode = 'add'; + component.ngOnChanges(); + expect(component.generalExaminationForm).toBeTruthy(); + expect(doctor.getGeneralExamintionData).not.toHaveBeenCalled(); + expect(doctor.updatePatientExamination).not.toHaveBeenCalled(); }); - it('should execute on changes of GeneralOpdExaminationComponent', () => { - component.ngOnChanges(); - expect(component).toBeTruthy(); + describe('view mode', () => { + const data = { + generalExamination: { pallor: 'Yes' }, + headToToeExamination: { head: 'Normal' }, + cardiovascularExamination: { x: 'cv' }, + gastrointestinalExamination: { g: 'gi' }, + obstetricExamination: { o: 'ob' }, + }; + + beforeEach(() => { + component.mode = 'view'; + spyOn(console, 'log'); + }); + + it('fetches data using session IDs and patches General OPD form', () => { + doctor.getGeneralExamintionData.and.returnValue( + of({ statusCode: 200, data }) + ); + component.ngOnChanges(); + expect(doctor.getGeneralExamintionData).toHaveBeenCalledWith('B1', 'V1'); + expect(form.value.generalExaminationForm.pallor).toBe('Yes'); + expect(form.value.headToToeExaminationForm.head).toBe('Normal'); + expect( + form.value.systemicExaminationForm.gastroIntestinalSystemForm.g + ).toBe('gi'); + expect( + form.value.systemicExaminationForm.obstetricExaminationForANCForm.o + ).toBe('ob'); + }); + + it('patches ANC data without gastro-intestinal section', () => { + component.visitCategory = 'ANC'; + doctor.getGeneralExamintionData.and.returnValue( + of({ statusCode: 200, data }) + ); + component.ngOnChanges(); + expect( + form.value.systemicExaminationForm.cardioVascularSystemForm.x + ).toBe('cv'); + expect( + form.value.systemicExaminationForm.obstetricExaminationForANCForm.o + ).toBe('ob'); + expect( + form.value.systemicExaminationForm.gastroIntestinalSystemForm.g + ).toBeNull(); + }); + + it('patches PNC data without obstetric section', () => { + component.visitCategory = 'PNC'; + doctor.getGeneralExamintionData.and.returnValue( + of({ statusCode: 200, data }) + ); + component.ngOnChanges(); + expect( + form.value.systemicExaminationForm.gastroIntestinalSystemForm.g + ).toBe('gi'); + expect( + form.value.systemicExaminationForm.obstetricExaminationForANCForm.o + ).toBeNull(); + }); + + it('does not patch for other categories', () => { + component.visitCategory = 'Cancer Screening'; + doctor.getGeneralExamintionData.and.returnValue( + of({ statusCode: 200, data }) + ); + component.ngOnChanges(); + expect(form.value.generalExaminationForm.pallor).toBeNull(); + }); + + it('ignores non-200 responses', () => { + doctor.getGeneralExamintionData.and.returnValue( + of({ statusCode: 500, data }) + ); + component.ngOnChanges(); + expect(form.value.generalExaminationForm.pallor).toBeNull(); + expect(console.log).not.toHaveBeenCalled(); + }); + + it('unsubscribes on destroy', () => { + component.ngOnChanges(); + const sub = component.ancExaminationDataSubscription; + spyOn(sub, 'unsubscribe'); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + }); }); - it('should execute on changes and check for mode', () => { - component.mode = new String('view'); - localStorage.setItem('visitID', '45'); - localStorage.setItem('beneficiaryRegID', '5476'); - spyOn(component, 'getAncExaminationData'); - component.ngOnChanges(); - fixture.detectChanges(); - const visitID = localStorage.getItem('visitID'); - const benRegID = localStorage.getItem('beneficiaryRegID'); - expect(component.getAncExaminationData).toHaveBeenCalled(); + it('ngOnDestroy is safe without a subscription', () => { + expect(() => component.ngOnDestroy()).not.toThrow(); }); - it('should get GeneralExaminationData when mode is view and patch data to fields', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '932'); - localStorage.setItem('beneficiaryRegID', '7397'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGeneralExamintionData').and.returnValue( - Observable.of(data.examinationGOPData.data) + describe('update mode', () => { + beforeEach(() => { + fixture.detectChanges(); + component.mode = 'update'; + }); + + it('updates the examination and marks the form pristine on success', () => { + form.markAsDirty(); + doctor.updatePatientExamination.and.returnValue( + of({ statusCode: 200, data: {} }) ); - spyOn(component, 'getAncExaminationData').and.callThrough(); - component.mode = new String('view'); component.ngOnChanges(); - expect(component.getAncExaminationData).toHaveBeenCalled(); - expect(doctorService.getGeneralExamintionData).toHaveBeenCalled(); - fixture.detectChanges(); - // expect(component.patientExaminationForm.value.generalExaminationForm).toEqual(data.examinationData.data.generalExamination) - }) - )); + expect(doctor.updatePatientExamination).toHaveBeenCalledWith( + form.value, + 'General OPD', + { + beneficiaryRegID: 'B1', + benVisitID: 'V1', + providerServiceMapID: 'P1', + modifiedBy: 'nurse', + beneficiaryID: 'BEN', + sessionID: 'S1', + parkingPlaceID: 9, + vanID: 7, + benFlowID: 'F1', + visitCode: 'VC1', + } + ); + expect(confirm.alert).toHaveBeenCalledWith( + 'Examination updated successfully', + 'success' + ); + expect(form.pristine).toBeTrue(); + }); - it('should hide systemic examination for visitCategory category NCD Care', () => { - component.visitCategory = 'NCD Care'; - component.ngOnInit(); + it('alerts error when response has null data', () => { + doctor.updatePatientExamination.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.ngOnChanges(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Error in Examination update', + 'error' + ); + }); + + it('alerts error when the call fails', () => { + doctor.updatePatientExamination.and.returnValue(throwingObs()); + component.ngOnChanges(); + expect(confirm.alert).toHaveBeenCalledWith( + 'Error in Examination update', + 'error' + ); + }); + + it('notifies mandatory fields and skips update when invalid', () => { + form = buildForm(true); + component.patientExaminationForm = form; + component.ngOnChanges(); + const g = + LANGUAGE_EN.ExaminationData.ANC_OPD_PNCExamination.genExamination; + expect(confirm.notify).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mandatoryFields, + [ + g.dangersigns, + g.lymph, + g.typeofLymphadenopathy, + g.extentofEdema, + g.typeofEdema, + ] + ); + expect(doctor.updatePatientExamination).not.toHaveBeenCalled(); + }); + }); + + it('checkRequired returns true when there are no errors', () => { fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('nurse-systemic-examination')); - expect(de).not.toBeTruthy(); + expect(component.checkRequired(form)).toBeTrue(); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/head-to-toe-examination/head-to-toe-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/head-to-toe-examination/head-to-toe-examination.component.spec.ts index 6772b806..659d353b 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/head-to-toe-examination/head-to-toe-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/head-to-toe-examination/head-to-toe-examination.component.spec.ts @@ -19,337 +19,101 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { HeadToToeExaminationComponent } from './head-to-toe-examination.component'; -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService } from '../../../shared/services'; - -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { MaterialModule } from '../../../../core/material.module'; +import { GeneralUtils } from '../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('HeadToToeExaminationComponent', () => { let component: HeadToToeExaminationComponent; let fixture: ComponentFixture; - let doctorService: DoctorService; - let confirmationService: ConfirmationService; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ + let tracking: any; + let form: FormGroup; + + const fields = [ + 'head', + 'eyes', + 'ears', + 'nose', + 'oralCavity', + 'throat', + 'breastAndNipples', + 'trunk', + 'upperLimbs', + 'lowerLimbs', + 'skin', + 'hair', + 'nails', + ]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [HeadToToeExaminationComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], - providers: [ - ConfirmationService, - { provide: DoctorService, useClass: DoctorServiceStub }, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(HeadToToeExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.headToToeExaminationForm = new GeneralUtils( - fb - ).createHeadToToeExaminationForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createHeadToToeExaminationForm(); + component.headToToeExaminationDataForm = form; fixture.detectChanges(); }); - it('should create HeadToToeExaminationComponent', () => { - expect(component).toBeTruthy(); + it('creates and loads language', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('should initialize HeadToToeExaminationComponent', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); - }); - - it('should check head to toe examination and make dependentfield null', async(() => { - spyOn(component, 'checkWithHeadToToe').and.callThrough(); - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - component.headToToeExaminationForm.patchValue({ head: 'Head' }); - component.headToToeExaminationForm.patchValue({ eyes: 'Eyes' }); - component.headToToeExaminationForm.patchValue({ ears: 'Ears' }); - component.headToToeExaminationForm.patchValue({ nose: 'Nose' }); - component.headToToeExaminationForm.patchValue({ - oralCavity: 'Oral Cavity', - }); - component.headToToeExaminationForm.patchValue({ throat: 'Throat' }); - component.headToToeExaminationForm.patchValue({ - breastAndNipples: 'Breast', - }); - component.headToToeExaminationForm.patchValue({ trunk: 'Trunk' }); - component.headToToeExaminationForm.patchValue({ - upperLimbs: 'Upper Limbs', - }); - component.headToToeExaminationForm.patchValue({ - lowerLimbs: 'Lower Limbs', - }); - component.headToToeExaminationForm.patchValue({ skin: 'Skin' }); - component.headToToeExaminationForm.patchValue({ hair: 'Hairs' }); - component.headToToeExaminationForm.patchValue({ nails: 'Nails' }); - debugElement = fixture.debugElement.query(By.css('#headtoToeExam')); - debugElement.nativeElement.value = 'Abnormal'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.headtoToeExam).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWithHeadToToe).toHaveBeenCalled(); - expect(component.head).toEqual(null); - expect(component.eyes).toEqual(null); - expect(component.ears).toEqual(null); - expect(component.nose).toEqual(null); - expect(component.oralCavity).toEqual(null); - expect(component.throat).toEqual(null); - expect(component.breastAndNipples).toEqual(null); - expect(component.trunk).toEqual(null); - expect(component.upperLimbs).toEqual(null); - expect(component.lowerLimbs).toEqual(null); - expect(component.skin).toEqual(null); - expect(component.hair).toEqual(null); - expect(component.nails).toEqual(null); - })); - - it('should enable head when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); + it('shows the detail fields only when exam is Abnormal', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('[formControlName="head"]')).toBeNull(); + form.patchValue({ headtoToeExam: 'Abnormal' }); fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#head')); - expect(debugElement).toBeTruthy(); + fields.forEach(f => + expect(el.querySelector(`[formControlName="${f}"]`)) + .withContext(f) + .not.toBeNull() + ); }); - it('should enable eyes when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#eyes')); - expect(debugElement).toBeTruthy(); + it('getters return the control values', () => { + const patch: any = { headtoToeExam: 'Abnormal' }; + fields.forEach(f => (patch[f] = `${f}-val`)); + form.patchValue(patch); + expect(component.headtoToeExam).toBe('Abnormal'); + fields.forEach(f => + expect((component as any)[f]) + .withContext(f) + .toBe(`${f}-val`) + ); }); - it('should enable ears when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#ears')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable nose when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#nose')); - expect(debugElement).toBeTruthy(); + it('checkWithHeadToToe clears every detail field', () => { + const patch: any = {}; + fields.forEach(f => (patch[f] = 'x')); + form.patchValue(patch); + component.checkWithHeadToToe(); + fields.forEach(f => expect(form.value[f]).withContext(f).toBeNull()); }); - it('should enable oralCavity when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#oralCavity')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable throat when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#throat')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable breastAndNipples when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#breastAndNipples')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable trunk when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#trunk')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable upperLimbs when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#upperLimbs')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable lowerLimbs when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lowerLimbs')); - expect(debugElement).toBeTruthy(); - }); - - it('should enable skin when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#skin')); - expect(debugElement).toBeTruthy(); - }); - it('should enable hair when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#hair')); - expect(debugElement).toBeTruthy(); - }); - it('should enable nails when headtoToeExam is Abnormal ', () => { - component.headToToeExaminationForm.patchValue({ - headtoToeExam: 'Abnormal', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#nails')); - expect(debugElement).toBeTruthy(); - }); - - it('should disable head when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#head')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable eyes when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#eyes')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable ears when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#ears')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable nose when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#nose')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable oralCavity when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#oralCavity')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable throat when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#throat')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable breastAndNipples when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#breastAndNipples')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable trunk when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#trunk')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable upperLimbs when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#upperLimbs')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable lowerLimbs when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#lowerLimbs')); - expect(debugElement).not.toBeTruthy(); - }); - - it('should disable skin when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#skin')); - expect(debugElement).not.toBeTruthy(); - }); - it('should disable hair when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#hair')); - expect(debugElement).not.toBeTruthy(); - }); - it('should disable nails when headtoToeExam is Normal ', () => { - component.headToToeExaminationForm.patchValue({ headtoToeExam: 'Normal' }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#nails')); - expect(debugElement).not.toBeTruthy(); + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Head'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Head', + 'Head To Toe Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/cardio-vascular-system/cardio-vascular-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/cardio-vascular-system/cardio-vascular-system.component.spec.ts index 89a0671b..719381b6 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/cardio-vascular-system/cardio-vascular-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/cardio-vascular-system/cardio-vascular-system.component.spec.ts @@ -19,72 +19,70 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { CardioVascularSystemComponent } from './cardio-vascular-system.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('CardioVascularSystemComponent', () => { let component: CardioVascularSystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [CardioVascularSystemComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(CardioVascularSystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.cardioVascularSystemForm = new GeneralUtils( - fb - ).createCardioVascularSystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createCardioVascularSystemForm(); + component.cardioVascularSystemDataForm = form; fixture.detectChanges(); }); - it('should create CardioVascularSystemComponent', () => { + it('creates and loads the language set', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('refreshes the language set on ngDoCheck', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('binds the apexbeatLocation input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="apexbeatLocation"]' + ) as HTMLInputElement; + expect(input).not.toBeNull(); + input.value = 'observed'; + input.dispatchEvent(new Event('input')); + expect(form.value.apexbeatLocation).toBe('observed'); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Field'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Field', + 'Cardio-Vascular System Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/central-nervous-system/central-nervous-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/central-nervous-system/central-nervous-system.component.spec.ts index 2c2f7582..ba21f06d 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/central-nervous-system/central-nervous-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/central-nervous-system/central-nervous-system.component.spec.ts @@ -19,72 +19,78 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { CentralNervousSystemComponent } from './central-nervous-system.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('CentralNervousSystemComponent', () => { let component: CentralNervousSystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [CentralNervousSystemComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(CentralNervousSystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.centralNervousSystemForm = new GeneralUtils( - fb - ).createCentralNervousSystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createCentralNervousSystemForm(); + component.centralNervousSystemDataForm = form; fixture.detectChanges(); }); - it('should create CentralNervousSystemComponent', () => { + it('creates and loads the language set', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('refreshes the language set on ngDoCheck', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('binds the motorSystem input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="motorSystem"]' + ) as HTMLInputElement; + expect(input).not.toBeNull(); + input.value = 'observed'; + input.dispatchEvent(new Event('input')); + expect(form.value.motorSystem).toBe('observed'); + }); + + it('exposes the selectHandedness options', () => { + expect(component.selectHandedness.map(o => o.name)).toEqual([ + 'No', + 'Right Handed', + 'Left Handed', + ]); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Field'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Field', + 'Central Nervous System Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/gastro-intestinal-system/gastro-intestinal-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/gastro-intestinal-system/gastro-intestinal-system.component.spec.ts index 3156d8e9..0d279c2f 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/gastro-intestinal-system/gastro-intestinal-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/gastro-intestinal-system/gastro-intestinal-system.component.spec.ts @@ -19,116 +19,91 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { GastroIntestinalSystemComponent } from './gastro-intestinal-system.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('GastroIntestinalSystemComponent', () => { let component: GastroIntestinalSystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [GastroIntestinalSystemComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GastroIntestinalSystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.gastroIntestinalSystemForm = new GeneralUtils( - fb - ).createGastroIntestinalSystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createGastroIntestinalSystemForm(); + component.gastroIntestinalSystemDataForm = form; fixture.detectChanges(); }); - it('should create GastroIntestinalSystemComponent', () => { - expect(component).toBeTruthy(); + it('creates and loads language', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.selectLiver.map(o => o.name)).toEqual([ + 'Not Palpable', + 'Just Palpable', + 'Enlarged', + ]); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('shows the tenderness location only when tenderness is Present', () => { + const el: HTMLElement = fixture.nativeElement; + const sel = '[formControlName="palpation_LocationOfTenderness"]'; + expect(el.querySelector(sel)).toBeNull(); + form.patchValue({ palpation_Tenderness: 'Present' }); + fixture.detectChanges(); + expect(el.querySelector(sel)).not.toBeNull(); }); - it('should enable Location of tenderness when Tenderness is present ', () => { - component.gastroIntestinalSystemForm.patchValue({ + it('getters return control values', () => { + form.patchValue({ palpation_Tenderness: 'Present', + palpation_LocationOfTenderness: 'RIF', }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#palpation_LocationOfTenderness') - ); - expect(debugElement).toBeTruthy(); + expect(component.palpation_Tenderness).toBe('Present'); + expect(component.palpation_LocationOfTenderness).toBe('RIF'); }); - it('should disable Location of tenderness when Tenderness is Absent ', () => { - component.gastroIntestinalSystemForm.patchValue({ - palpation_Tenderness: 'Absent', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#palpation_LocationOfTenderness') - ); - expect(debugElement).not.toBeTruthy(); + it('checkWithTenderness clears the tenderness location', () => { + form.patchValue({ palpation_LocationOfTenderness: 'RIF' }); + component.checkWithTenderness(); + expect(form.value.palpation_LocationOfTenderness).toBeNull(); }); - it('should check Tenderness and make dependentfield null', async(() => { - spyOn(component, 'checkWithTenderness').and.callThrough(); - component.gastroIntestinalSystemForm.patchValue({ - palpation_Tenderness: 'Present', - }); - component.gastroIntestinalSystemForm.patchValue({ - palpation_LocationOfTenderness: 'jhfgfhjhjhg', - }); - debugElement = fixture.debugElement.query(By.css('#palpation_Tenderness')); - debugElement.nativeElement.value = 'Present'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.palpation_Tenderness).toEqual( - debugElement.nativeElement.value + it('binds the inspection input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="inspection"]' + ) as HTMLInputElement; + input.value = 'Distended'; + input.dispatchEvent(new Event('input')); + expect(form.value.inspection).toBe('Distended'); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Liver'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Liver', + 'Gastro-Intestinal System Examination' ); - fixture.detectChanges(); - expect(component.checkWithTenderness).toHaveBeenCalled(); - expect(component.palpation_LocationOfTenderness).toEqual(null); - })); + }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/genito-urinary-system/genito-urinary-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/genito-urinary-system/genito-urinary-system.component.spec.ts index 689c08f7..ed673853 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/genito-urinary-system/genito-urinary-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/genito-urinary-system/genito-urinary-system.component.spec.ts @@ -19,72 +19,70 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { GenitoUrinarySystemComponent } from './genito-urinary-system.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('GenitoUrinarySystemComponent', () => { let component: GenitoUrinarySystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [GenitoUrinarySystemComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(GenitoUrinarySystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.genitoUrinarySystemForm = new GeneralUtils( - fb - ).createGenitoUrinarySystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createGenitoUrinarySystemForm(); + component.genitoUrinarySystemDataForm = form; fixture.detectChanges(); }); - it('should create GenitoUrinarySystemComponent', () => { + it('creates and loads the language set', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('refreshes the language set on ngDoCheck', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('binds the renalAngle input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="renalAngle"]' + ) as HTMLInputElement; + expect(input).not.toBeNull(); + input.value = 'observed'; + input.dispatchEvent(new Event('input')); + expect(form.value.renalAngle).toBe('observed'); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Field'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Field', + 'Genito-Urinary System Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/musculoskeletal-system/musculoskeletal-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/musculoskeletal-system/musculoskeletal-system.component.spec.ts index abe48bcf..7c94410f 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/musculoskeletal-system/musculoskeletal-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/musculoskeletal-system/musculoskeletal-system.component.spec.ts @@ -19,104 +19,99 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MusculoskeletalSystemComponent } from './musculoskeletal-system.component'; import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; - import { MasterdataService } from '../../../../shared/services'; - -import { MasterdataServiceStub } from '../../../../shared/mocks/masterdata-service-stub'; -import { MusculoskeletalSystemComponent } from './musculoskeletal-system.component'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('MusculoskeletalSystemComponent', () => { let component: MusculoskeletalSystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let masterData$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + masterData$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [MusculoskeletalSystemComponent], - schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], providers: [ - { provide: MasterdataService, useClass: MasterdataServiceStub }, + ...commonTestProviders(), + { + provide: MasterdataService, + useValue: { nurseMasterData$: masterData$ }, + }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(MusculoskeletalSystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.musculoSkeletalSystemForm = new GeneralUtils( - fb - ).createMusculoSkeletalSystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createMusculoSkeletalSystemForm(); + component.musculoSkeletalSystemDataForm = form; fixture.detectChanges(); }); - it('should create MusculoskeletalSystemComponent', () => { - expect(component).toBeTruthy(); + it('creates, loads language and keeps empty joint types without master data', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.selectTypeOfJoint).toEqual([]); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('loads joint types from nurse master data', () => { + const jointTypes = [{ name: 'Knee', id: 1 }]; + masterData$.next({ jointTypes }); + expect(component.selectTypeOfJoint).toEqual(jointTypes); }); - it('Should call getMasterData ', async () => { - spyOn(component, 'getMasterData'); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.getMasterData).toHaveBeenCalled(); + it('exposes laterality and abnormality options', () => { + expect(component.selectJointLaterality.length).toBe(3); + expect(component.selectUpperLimbsAbnormality.map(o => o.name)).toContain( + 'Deformity' + ); + expect(component.selectLowerLimbsLaterality[2].name).toBe('Bilateral'); + }); + + it('binds the spine input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="spine"]' + ) as HTMLInputElement; + input.value = 'Kyphosis'; + input.dispatchEvent(new Event('input')); + expect(form.value.spine).toBe('Kyphosis'); }); - it('Should call getMasterData and get master Data ', async( - inject([MasterdataService], masterdataService => { - spyOn(component, 'getMasterData').and.callThrough(); - spyOn(masterdataService, 'getNurseMasterData') - .and.returnValue(Observable.of(data.generalOPDNurseMasterdata.data)) - .and.callThrough(); - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data - ); - component.ngOnInit(); - fixture.detectChanges(); - expect(component.getMasterData).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.selectTypeOfJoint).toEqual( - data.generalOPDNurseMasterdata.data.jointTypes - ); - }) - )); + it('unsubscribes master data on destroy', () => { + const sub = component.nurseMasterDataSubscription; + spyOn(sub, 'unsubscribe').and.callThrough(); + component.ngOnDestroy(); + expect(sub.unsubscribe).toHaveBeenCalled(); + }); + + it('ngOnDestroy is safe without a subscription', () => { + component.nurseMasterDataSubscription = undefined; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Spine'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Spine', + 'Musculoskeletal System Examination' + ); + }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/obstetric-examination/obstetric-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/obstetric-examination/obstetric-examination.component.spec.ts index 212965d4..a41708c5 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/obstetric-examination/obstetric-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/obstetric-examination/obstetric-examination.component.spec.ts @@ -19,71 +19,86 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { ObstetricExaminationComponent } from './obstetric-examination.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('ObstetricExaminationComponent', () => { let component: ObstetricExaminationComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [ObstetricExaminationComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(ObstetricExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.obstetricExaminationForANCForm = new GeneralUtils( - fb - ).createObstetricExaminationForANCForm(); + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createObstetricExaminationForANCForm(); + component.obstetricExaminationForANCDataForm = form; + spyOn(console, 'log'); + fixture.detectChanges(); + }); + + it('creates and loads language', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.selectFundalHeight.length).toBe(9); + expect(component.selectFetalHeartRate.map(o => o.name)).toEqual([ + '<120', + '120-160', + '>160', + ]); + }); + + it('shows fetal heart rate only when heart sounds are Audible', () => { + const el: HTMLElement = fixture.nativeElement; + const sel = '[formControlName="fetalHeartRate_BeatsPerMinute"]'; + expect(el.querySelector(sel)).toBeNull(); + form.patchValue({ fetalHeartSounds: 'Audible' }); fixture.detectChanges(); + expect(el.querySelector(sel)).not.toBeNull(); }); - it('should create ObstetricExaminationComponent', () => { - expect(component).toBeTruthy(); + + it('resetFetalHeartRate clears the rate when Not Audible', () => { + form.patchValue({ fetalHeartRate_BeatsPerMinute: '>160' }); + component.resetFetalHeartRate({ value: 'Not Audible' }); + expect(form.value.fetalHeartRate_BeatsPerMinute).toBeNull(); + }); + + it('resetFetalHeartRate keeps the rate when Audible', () => { + form.patchValue({ fetalHeartRate_BeatsPerMinute: '>160' }); + component.resetFetalHeartRate({ value: 'Audible' }); + expect(form.value.fetalHeartRate_BeatsPerMinute).toBe('>160'); + }); + + it('getters return control values', () => { + form.patchValue({ fetalHeartSounds: 'Audible', sfh: 24 }); + expect(component.fetalHeartSounds).toBe('Audible'); + expect(component.SFH).toBe(24); + expect(console.log).toHaveBeenCalledWith('sfh'); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('binds the sfh input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="sfh"]' + ) as HTMLInputElement; + input.value = '30'; + input.dispatchEvent(new Event('input')); + expect(String(form.value.sfh)).toBe('30'); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/respiratory-system/respiratory-system.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/respiratory-system/respiratory-system.component.spec.ts index 9e6877b1..87248106 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/respiratory-system/respiratory-system.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/respiratory-system/respiratory-system.component.spec.ts @@ -19,72 +19,79 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { GeneralUtils } from '../../../../shared/utility'; - -import * as data from '../../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { RespiratorySystemComponent } from './respiratory-system.component'; +import { MaterialModule } from '../../../../../core/material.module'; +import { GeneralUtils } from '../../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('RespiratorySystemComponent', () => { let component: RespiratorySystemComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [RespiratorySystemComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(RespiratorySystemComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.respiratorySystemForm = new GeneralUtils( - fb - ).createRespiratorySystemForm(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + } as any).createRespiratorySystemForm(); + component.respiratorySystemDataForm = form; fixture.detectChanges(); }); - it('should create RespiratorySystemComponent', () => { + it('creates and loads the language set', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + it('refreshes the language set on ngDoCheck', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('binds the palpation input to the form', () => { + const input = fixture.nativeElement.querySelector( + '[formControlName="palpation"]' + ) as HTMLInputElement; + expect(input).not.toBeNull(); + input.value = 'observed'; + input.dispatchEvent(new Event('input')); + expect(form.value.palpation).toBe('observed'); + }); + + it('exposes the selectPercussion options', () => { + expect(component.selectPercussion.map(o => o.name)).toEqual([ + 'Dull', + 'Stony Dull', + 'Resonant', + 'Hyper Resonant', + ]); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Field'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Field', + 'Respiratory System Examination' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/systemic-examination.component.spec.ts b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/systemic-examination.component.spec.ts index 011c9c57..6a6086fa 100644 --- a/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/systemic-examination.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/examination/general-opd-examination/systemic-examination/systemic-examination.component.spec.ts @@ -19,155 +19,145 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../../core/material.module'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { SystemicExaminationComponent } from './systemic-examination.component'; - -import { GeneralUtils } from '../../../shared/utility'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { GeneralUtils } from '../../../shared/utility/general-utility'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('SystemicExaminationComponent', () => { let component: SystemicExaminationComponent; let fixture: ComponentFixture; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; + let tracking: any; + let form: FormGroup; + const serviceLineDetails = JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [SystemicExaminationComponent], + providers: [...commonTestProviders({ session: { serviceLineDetails } })], schemas: [NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(SystemicExaminationComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - fb = debugElement.injector.get(FormBuilder); - component.systemicExaminationForm = new GeneralUtils( - fb - ).createSystemicExaminationForm(); - fixture.detectChanges(); - }); - - it('should create SystemicExaminationComponent', () => { - expect(component).toBeTruthy(); + tracking = TestBed.inject(AmritTrackingService) as any; + form = new GeneralUtils(new FormBuilder(), { + getItem: () => serviceLineDetails, + } as any).createSystemicExaminationForm(); + component.systemicExaminationDataForm = form; }); - it('Should initialize the component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); - }); - - it('should execute on changes of SystemicExaminationComponent', () => { - component.ngOnChanges(); - expect(component).toBeTruthy(); - }); + function expectSubForms() { + expect(component.gastroIntestinalSystemForm).toBe( + form.get('gastroIntestinalSystemForm') as FormGroup + ); + expect(component.cardioVascularSystemForm).toBe( + form.get('cardioVascularSystemForm') as FormGroup + ); + expect(component.respiratorySystemForm).toBe( + form.get('respiratorySystemForm') as FormGroup + ); + expect(component.centralNervousSystemForm).toBe( + form.get('centralNervousSystemForm') as FormGroup + ); + expect(component.musculoSkeletalSystemForm).toBe( + form.get('musculoSkeletalSystemForm') as FormGroup + ); + expect(component.genitoUrinarySystemForm).toBe( + form.get('genitoUrinarySystemForm') as FormGroup + ); + } - it('Should check visit category while initializing', () => { + it('ANC init adds the obstetric form and renders it', () => { component.visitCategory = 'ANC'; - component.ngOnInit(); fixture.detectChanges(); - expect(component.displayANC).toEqual(true); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.displayANC).toBeTrue(); + expect(component.displayGeneral).toBeFalse(); + expect(form.get('obstetricExaminationForANCForm')).toBeTruthy(); + expect(component.obstetricExaminationForANCForm).toBe( + form.get('obstetricExaminationForANCForm') as FormGroup + ); + expect(form.value.obstetricExaminationForANCForm.vanID).toBe(1); + expectSubForms(); + const el: HTMLElement = fixture.nativeElement; + expect( + el.querySelector('app-nurse-anc-obstetric-examination') + ).not.toBeNull(); + expect(el.querySelector('app-nurse-gastro-intestinal-system')).toBeNull(); }); - it('Should check visit category while initializing and enable obtestric examination', () => { - component.visitCategory = 'ANC'; - component.ngOnInit(); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-anc-obstetric-examination') - ); - expect(component.displayANC).toEqual(true); - expect(de.nativeElement).toBeTruthy(); + ['General OPD', 'PNC'].forEach(cat => { + it(`${cat} init shows the general systems`, () => { + component.visitCategory = cat; + fixture.detectChanges(); + expect(component.displayANC).toBeFalse(); + expect(component.displayGeneral).toBeTrue(); + expect(component.obstetricExaminationForANCForm).toBeNull(); + expectSubForms(); + const el: HTMLElement = fixture.nativeElement; + expect( + el.querySelector('app-nurse-gastro-intestinal-system') + ).not.toBeNull(); + expect( + el.querySelector('app-nurse-genito-urinary-system') + ).not.toBeNull(); + }); }); - it('Should check visit category while initializing and disable obtestric examination', () => { - component.visitCategory = 'General OPD'; - component.ngOnInit(); + it('other categories show neither section', () => { + component.visitCategory = 'NCD Care'; fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-anc-obstetric-examination') - ); - expect(component.displayANC).toEqual(false); - expect(component.displayGeneral).toEqual(true); - expect(de).not.toBeTruthy(); + expect(component.displayANC).toBeFalse(); + expect(component.displayGeneral).toBeFalse(); }); - it('Should check visit category while on changes and enable obtestric examination', () => { + it('ngOnChanges adds obstetric form for ANC', () => { component.visitCategory = 'ANC'; component.ngOnChanges(); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-anc-obstetric-examination') - ); - expect(component.displayANC).toEqual(true); - expect(de.nativeElement).toBeTruthy(); + expect(component.displayANC).toBeTrue(); + expect(form.get('obstetricExaminationForANCForm')).toBeTruthy(); + expect(component.obstetricExaminationForANCForm).toBeTruthy(); }); - it('Should check visit category while on changes and disable obtestric examination', () => { - component.visitCategory = 'General OPD'; + it('ngOnChanges removes obstetric form and shows general for PNC', () => { + component.visitCategory = 'ANC'; component.ngOnChanges(); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-anc-obstetric-examination') - ); - expect(component.displayANC).toEqual(false); - expect(component.displayGeneral).toEqual(true); - expect(de).not.toBeTruthy(); + component.visitCategory = 'PNC'; + component.ngOnChanges(); + expect(component.displayANC).toBeFalse(); + expect(component.displayGeneral).toBeTrue(); + expect(form.get('obstetricExaminationForANCForm')).toBeNull(); }); - it('Should check visit category while on changes and enable Gastro intestinal examination', () => { + it('ngOnChanges for General OPD sets displayGeneral', () => { component.visitCategory = 'General OPD'; component.ngOnChanges(); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-gastro-intestinal-system') - ); - expect(component.displayANC).toEqual(false); - expect(component.displayGeneral).toEqual(true); - expect(de.nativeElement).toBeTruthy(); + expect(component.displayGeneral).toBeTrue(); }); - it('Should check visit category while on changes and disable Gastro intestinal examination', () => { - component.visitCategory = 'ANC'; + it('ngOnChanges for other categories leaves displayGeneral unset', () => { + component.visitCategory = 'Cancer Screening'; component.ngOnChanges(); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-gastro-intestinal-system') + expect(component.displayANC).toBeFalse(); + expect(component.displayGeneral).toBeFalsy(); + expectSubForms(); + }); + + it('trackFieldInteraction reports to the tracking service', () => { + component.trackFieldInteraction('Spine'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Spine', + 'Systemic Examination' ); - expect(component.displayANC).toEqual(true); - expect(component.displayGeneral).toEqual(false); - expect(de).not.toBeTruthy(); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/cancer-history/cancer-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/cancer-history/cancer-history.component.spec.ts new file mode 100644 index 00000000..c7854314 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/history/cancer-history/cancer-history.component.spec.ts @@ -0,0 +1,293 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { of, throwError, BehaviorSubject } from 'rxjs'; +import { CancerHistoryComponent } from './cancer-history.component'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { HttpServiceService } from 'src/app/app-modules/core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +describe('CancerHistoryComponent', () => { + let component: CancerHistoryComponent; + let fixture: ComponentFixture; + let fb: FormBuilder; + let mockDoctorService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: any; + let mockHttpServiceService: any; + let mockSessionStorage: jasmine.SpyObj; + let beneficiarySubject: BehaviorSubject; + + function buildCancerHistoryForm(): FormGroup { + return fb.group({ + cancerPatientFamilyMedicalHistoryForm: fb.group({ + diseases: [''], + }), + cancerPatientPerosnalHistoryForm: fb.group({ + tobaccoUse: [''], + alcoholUse: [''], + }), + cancerPatientObstetricHistoryForm: fb.group({ + pregnancyStatus: [''], + }), + }); + } + + beforeEach(waitForAsync(() => { + fb = new FormBuilder(); + beneficiarySubject = new BehaviorSubject(null); + + mockDoctorService = jasmine.createSpyObj('DoctorService', [ + 'getCancerHistoryDetails', + 'updateCancerHistoryDetails', + ]); + + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + + mockBeneficiaryDetailsService = { + beneficiaryDetails$: beneficiarySubject.asObservable(), + }; + + mockHttpServiceService = { + currentLangugae$: new BehaviorSubject({ language: 'English' }), + }; + + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [CancerHistoryComponent], + providers: [ + { provide: DoctorService, useValue: mockDoctorService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CancerHistoryComponent); + component = fixture.componentInstance; + component.nurseCancerHistoryForm = buildCancerHistoryForm(); + component.mode = 'new'; + component.pregnancyStatus = ''; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should initialize sub-form references', () => { + component.ngOnInit(); + expect(component.cancerPatientFamilyMedicalHistoryForm).toBeTruthy(); + expect(component.cancerPatientPerosnalHistoryForm).toBeTruthy(); + expect(component.cancerPatientObstetricHistoryForm).toBeTruthy(); + }); + + it('should subscribe to beneficiary details', () => { + component.ngOnInit(); + const beneficiary = { name: 'Test Patient' }; + beneficiarySubject.next(beneficiary); + expect(component.templateBeneficiaryDetails).toEqual(beneficiary); + }); + + it('should set language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toBeTruthy(); + }); + }); + + describe('ngOnChanges', () => { + it('should fetch cancer history in view mode', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'visitID') return '123'; + if (key === 'beneficiaryRegID') return '456'; + return null; + }); + mockDoctorService.getCancerHistoryDetails.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.mode = 'view'; + component.ngOnChanges({}); + expect(mockDoctorService.getCancerHistoryDetails).toHaveBeenCalledWith( + '456', + '123' + ); + }); + + it('should call updatePateintHistory in update mode', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 1, parkingPlaceID: 2 }); + return 'testValue'; + }); + mockDoctorService.updateCancerHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.mode = 'update'; + component.ngOnChanges({}); + expect(mockDoctorService.updateCancerHistoryDetails).toHaveBeenCalled(); + }); + }); + + describe('updatePateintHistory', () => { + beforeEach(() => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 10, parkingPlaceID: 20 }); + return 'testVal'; + }); + }); + + it('should show success on successful update', () => { + mockDoctorService.updateCancerHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'History updated' } }) + ); + component.updatePateintHistory(component.nurseCancerHistoryForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'History updated', + 'success' + ); + }); + + it('should show error on failed update', () => { + mockDoctorService.updateCancerHistoryDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'Server error' }) + ); + component.updatePateintHistory(component.nurseCancerHistoryForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server error', + 'error' + ); + }); + + it('should show error on HTTP error', () => { + mockDoctorService.updateCancerHistoryDetails.and.returnValue( + throwError('Network error') + ); + component.updatePateintHistory(component.nurseCancerHistoryForm); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + }); + + describe('getCancerHistory', () => { + it('should patch form values on successful response', () => { + const historyData = { + statusCode: 200, + data: { + benPersonalHistory: { tobaccoUse: 'Yes' }, + benPersonalDietHistory: {}, + benFamilyHistory: [{ diseaseType: 'Breast Cancer' }], + benObstetricHistory: { pregnancyStatus: 'Yes' }, + }, + }; + mockDoctorService.getCancerHistoryDetails.and.returnValue( + of(historyData) + ); + component.ngOnInit(); + component.getCancerHistory('456', '123'); + expect(component.familyHistoryData).toEqual([ + { diseaseType: 'Breast Cancer' }, + ]); + }); + + it('should not patch when data is null', () => { + mockDoctorService.getCancerHistoryDetails.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.ngOnInit(); + component.getCancerHistory('456', '123'); + expect(component.familyHistoryData).toBeUndefined(); + }); + }); + + describe('getBenificiaryDetails', () => { + it('should set templateBeneficiaryDetails when beneficiary data exists', () => { + component.ngOnInit(); + beneficiarySubject.next({ name: 'John' }); + expect(component.templateBeneficiaryDetails).toEqual({ name: 'John' }); + }); + + it('should not set templateBeneficiaryDetails when null', () => { + component.ngOnInit(); + beneficiarySubject.next(null); + expect(component.templateBeneficiaryDetails).toBeUndefined(); + }); + }); + + describe('ngOnDestroy', () => { + it('should unsubscribe from beneficiary details', () => { + component.ngOnInit(); + component.ngOnDestroy(); + expect(component.beneficiaryDetailsSubscription).toBeDefined(); + }); + + it('should unsubscribe from cancer history if exists', () => { + mockDoctorService.getCancerHistoryDetails.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.ngOnInit(); + component.getCancerHistory('1', '2'); + component.ngOnDestroy(); + expect(component.cancerHistorySubscription).toBeDefined(); + }); + + it('should handle undefined subscriptions gracefully', () => { + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('ngDoCheck', () => { + it('should call fetchLanguageResponse', () => { + spyOn(component, 'fetchLanguageResponse'); + component.ngDoCheck(); + expect(component.fetchLanguageResponse).toHaveBeenCalled(); + }); + }); + + describe('fetchLanguageResponse', () => { + it('should set currentLanguageSet from language service', () => { + component.fetchLanguageResponse(); + expect(component.currentLanguageSet).toBeTruthy(); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/history/cancer-history/family-disease-history/family-disease-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/cancer-history/family-disease-history/family-disease-history.component.spec.ts index c748e216..c37c5707 100644 --- a/src/app/app-modules/nurse-doctor/history/cancer-history/family-disease-history/family-disease-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/cancer-history/family-disease-history/family-disease-history.component.spec.ts @@ -20,27 +20,368 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { FamilyDiseaseHistoryComponent } from './family-disease-history.component'; -describe('FamilyDiseaseHistoryComponent', () => { +describe('Cancer FamilyDiseaseHistoryComponent', () => { let component: FamilyDiseaseHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; + let A: any, B: any, OTHER: any; + let members: any[]; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [FamilyDiseaseHistoryComponent], - }).compileComponents(); - })); + function masterData() { + A = { + cancerDiseaseType: 'Breast', + gender: 'female', + snomedCode: '1', + snomedTerm: 'breast', + }; + B = { + cancerDiseaseType: 'Oral', + gender: 'unisex', + snomedCode: '2', + snomedTerm: 'oral', + }; + OTHER = { cancerDiseaseType: 'Any other Cancer', gender: 'unisex' }; + members = [ + { name: 'Mother', gender: 'female' }, + { name: 'Father', gender: 'male' }, + ]; + return { CancerDiseaseType: [A, OTHER, B], familyMemberTypes: members }; + } - beforeEach(() => { + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [FamilyDiseaseHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: '5', + visitID: '6', + serviceLineDetails: JSON.stringify({ + vanID: 1, + parkingPlaceID: 2, + }), + }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(FamilyDiseaseHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(FamilyDiseaseHistoryComponent); component = fixture.componentInstance; + form = new FormGroup({ + diseases: new FormArray([component.formUtils.initDiseases()]), + }); + component.cancerPatientFamilyMedicalHistoryForm = form; + component.mode = mode; fixture.detectChanges(); + } + + const diseases = () => form.controls['diseases'] as FormArray; + + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('ignores null master data and does not fetch history', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.templateNurseMasterData).toBeUndefined(); + expect(doctor.getCancerHistoryDetails).not.toHaveBeenCalled(); + }); + + it('maps master data into template lists', () => { + const m = masterData(); + master$.next(m); + expect(component.templateCancerDiseaseType).toBe(m.CancerDiseaseType); + expect(component.filterCancerDiseaseType).toEqual(m.CancerDiseaseType); + expect(component.templateFamilyMemberType).toBe(members); + expect(component.filterFamilyMemebers).toBe(members); + expect(component.temp[0].diseaseType).toEqual(m.CancerDiseaseType); + expect(doctor.getCancerHistoryDetails).not.toHaveBeenCalled(); + }); + + it('logs beneficiary details when present', () => { + spyOn(console, 'log'); + ben$.next({ ageVal: 3 }); + expect(console.log).toHaveBeenCalledWith('beneficiary', { ageVal: 3 }); + }); + + it('getDiseases returns controls or null', () => { + expect(component.getDiseases()?.length).toBe(1); + component.cancerPatientFamilyMedicalHistoryForm = new FormGroup({ + diseases: new FormControl(null), + }); + expect(component.getDiseases()).toBeNull(); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ngOnDestroy unsubscribes all active subscriptions', () => { + doctor.getCancerHistoryDetails.and.returnValue(of({ statusCode: 500 })); + master$.next(masterData()); + component.getCancerHistory('5', '6'); + const subs = [ + component.nurseMasterDataSubscription, + component.beneficiaryDetailSubscription, + component.cancerHistorySubscription, + ].map(s => spyOn(s, 'unsubscribe')); + component.ngOnDestroy(); + subs.forEach(s => expect(s).toHaveBeenCalled()); + }); + + it('ngOnDestroy tolerates missing subscriptions', () => { + component.nurseMasterDataSubscription = null; + component.beneficiaryDetailSubscription = null; + component.cancerHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + describe('with master data', () => { + beforeEach(() => master$.next(masterData())); + + it('filterFamilyMember patches snomed and removes type from other rows', () => { + component.addFamilyDisease(); + const row = diseases().at(0); + component.filterFamilyMember(A, 0, row); + expect(row.value.snomedCode).toBe('1'); + expect(row.value.snomedTerm).toBe('breast'); + expect(component.temp[1].diseaseType).not.toContain(A); + expect(component.temp[0].familyMembers).toEqual([members[0]]); + expect(component.previousValue[0]).toBe(A); + }); + + it('filterFamilyMember re-adds previous value to other rows', () => { + component.addFamilyDisease(); + const row = diseases().at(0); + component.filterFamilyMember(A, 0, row); + component.filterFamilyMember(B, 0, row); + expect(component.temp[1].diseaseType).toContain(A); + expect(component.temp[1].diseaseType).not.toContain(B); + expect( + component.temp[1].diseaseType.map((d: any) => d.cancerDiseaseType) + ).toEqual(['Any other Cancer', 'Breast']); + expect(component.temp[0].familyMembers).toEqual(members); + }); + + it('filterFamilyMember clears snomed for Any other Cancer', () => { + const row = diseases().at(0); + row.patchValue({ snomedCode: 'x', snomedTerm: 'y' }); + component.filterFamilyMember(OTHER, 0, row); + expect(row.value.snomedCode).toBeNull(); + expect(row.value.snomedTerm).toBeNull(); + }); + + it('filterFamilyMember without a form only filters members', () => { + component.filterFamilyMember(A, 0); + expect(component.filterFamilyMemebers).toEqual([members[0]]); + }); + + it('addFamilyDisease excludes already-selected types', () => { + diseases().at(0).patchValue({ cancerDiseaseType: A }); + component.addFamilyDisease(); + expect(diseases().length).toBe(2); + expect(component.temp[1].diseaseType).toEqual([OTHER, B]); + }); + + it('addFamilyDisease keeps Any other Cancer available', () => { + diseases().at(0).patchValue({ cancerDiseaseType: OTHER }); + component.addFamilyDisease(); + expect(component.temp[1].diseaseType).toEqual([A, OTHER, B]); + }); + + describe('removeDisease', () => { + it('does nothing when not confirmed', () => { + confirm.confirm.and.returnValue(of(false)); + component.addFamilyDisease(); + component.removeDisease(1, diseases().at(1)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(diseases().length).toBe(2); + }); + + it('resets the only row', () => { + const row = diseases().at(0); + row.patchValue({ cancerDiseaseType: A, familyMemberList: ['x'] }); + component.removeDisease(0, row); + expect(row.value.cancerDiseaseType).toBeNull(); + expect(row.value.familyMemberList).toBeNull(); + expect(diseases().length).toBe(1); + }); + + it('removes row and restores its type to other lists', () => { + component.addFamilyDisease(); + const row = diseases().at(1); + row.patchValue({ cancerDiseaseType: A }); + component.filterFamilyMember(A, 1, row); + expect(component.temp[0].diseaseType).not.toContain(A); + component.removeDisease(1, row); + expect(diseases().length).toBe(1); + expect(component.temp.length).toBe(1); + expect(component.temp[0].diseaseType).toContain(A); + }); + + it('removes a row without selected type', () => { + component.addFamilyDisease(); + component.removeDisease(1, diseases().at(1)); + expect(diseases().length).toBe(1); + }); + + it('removes an Any-other row without touching lists', () => { + component.addFamilyDisease(); + diseases().at(1).patchValue({ cancerDiseaseType: OTHER }); + const before = component.temp[0].diseaseType.slice(); + component.removeDisease(1, diseases().at(1)); + expect(component.temp[0].diseaseType).toEqual(before); + expect(diseases().length).toBe(1); + }); + }); + }); + + it('sortDiseaseList orders by type and handles equals', () => { + const list = [ + { cancerDiseaseType: 'b' }, + { cancerDiseaseType: 'a' }, + { cancerDiseaseType: 'b' }, + ]; + component.sortDiseaseList(list); + expect(list.map(l => l.cancerDiseaseType)).toEqual(['a', 'b', 'b']); + }); + + it('checkValidity is false only when type and members are set', () => { + expect(component.checkValidity({ value: null })).toBeTrue(); + expect( + component.checkValidity({ value: { cancerDiseaseType: 'a' } }) + ).toBeTrue(); + expect( + component.checkValidity({ + value: { cancerDiseaseType: 'a', familyMemberList: ['m'] }, + }) + ).toBeFalse(); + }); + + describe('getPreviousCancerFamilyHistory', () => { + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousCancerFamilyHistory.and.returnValue( + of({ data: payload }) + ); + component.getPreviousCancerFamilyHistory(); + expect(nurse.getPreviousCancerFamilyHistory).toHaveBeenCalledWith('5'); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: LANGUAGE_EN.common.prevFamilyHistory, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousCancerFamilyHistory.and.returnValue( + of({ data: { data: [] } }) + ); + component.getPreviousCancerFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.previousInfo + ); + }); + it('alerts errorStatus when data null', () => { + nurse.getPreviousCancerFamilyHistory.and.returnValue( + of({ data: null, errorStatus: 'bad' }) + ); + component.getPreviousCancerFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousCancerFamilyHistory.and.returnValue(throwingObs('e')); + component.getPreviousCancerFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith('e', 'error'); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('fetches and patches cancer family history', () => { + doctor.getCancerHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + benFamilyHistory: [ + { cancerDiseaseType: 'Breast', familyMemberList: ['Mother'] }, + { cancerDiseaseType: 'Custom' }, + { cancerDiseaseType: null }, + ], + }, + }) + ); + master$.next(masterData()); + expect(doctor.getCancerHistoryDetails).toHaveBeenCalledWith('5', '6'); + expect(diseases().length).toBe(3); + expect(diseases().at(0).value.cancerDiseaseType).toBe(A); + expect(diseases().at(0).value.familyMemberList).toEqual(['Mother']); + expect(diseases().at(0).touched).toBeTrue(); + expect(diseases().at(1).value.cancerDiseaseType).toBe(OTHER); + expect(diseases().at(1).value.otherDiseaseType).toBe('Custom'); + expect(diseases().at(2).value.cancerDiseaseType).toBeNull(); + }); + + it('ignores non-200 history', () => { + doctor.getCancerHistoryDetails.and.returnValue(of({ statusCode: 500 })); + master$.next(masterData()); + expect(component.familyHistoryData).toBeUndefined(); + expect(diseases().length).toBe(1); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/cancer-history/obstetric-history/obstetric-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/cancer-history/obstetric-history/obstetric-history.component.spec.ts index a111e919..a231267c 100644 --- a/src/app/app-modules/nurse-doctor/history/cancer-history/obstetric-history/obstetric-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/cancer-history/obstetric-history/obstetric-history.component.spec.ts @@ -20,27 +20,288 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { ObstetricHistoryComponent } from './obstetric-history.component'; -describe('ObstetricHistoryComponent', () => { +const CONTROLS = [ + 'pregnancyStatus', + 'isUrinePregTest', + 'pregnant_No', + 'noOfLivingChild', + 'isAbortion', + 'menstrualCycleLength', + 'menstrualFlowDuration', + 'menarche_Age', + 'menopauseAge', + 'isPostMenopauseBleeding', +]; + +describe('Cancer ObstetricHistoryComponent', () => { let component: ObstetricHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; + const info = LANGUAGE_EN.alerts.info; + const ev = (v: any) => ({ target: { value: v } }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject({ m: 1 }); + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ObstetricHistoryComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ session: { beneficiaryRegID: '9' } }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: autoSpy(DoctorService) }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(ObstetricHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(ObstetricHistoryComponent); component = fixture.componentInstance; + const g: any = {}; + CONTROLS.forEach(c => (g[c] = new FormControl(null))); + form = new FormGroup(g); + component.cancerPatientObstetricHistoryForm = form; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('loads master data, language and beneficiary on init', () => { + expect(component.templateNurseMasterData).toEqual({ m: 1 }); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.templateBeneficiaryDetails).toBeUndefined(); + ben$.next({ ageVal: 45 }); + expect(component.patientAge).toBe(45); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ngOnChanges sets pregnancy status only when provided', () => { + component.ispregnant = ''; + component.ngOnChanges(); + expect(form.value.pregnancyStatus).toBeNull(); + component.ispregnant = 'Yes'; + component.ngOnChanges(); + expect(form.value.pregnancyStatus).toBe('Yes'); + expect(component.pregnancyStatus).toBe('Yes'); + }); + + it('ngOnDestroy unsubscribes master data', () => { + const s = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(s).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('reset helpers clear dependent fields', () => { + form.patchValue({ + isUrinePregTest: 'Y', + noOfLivingChild: 2, + isAbortion: 'N', + }); + component.checkWithPregnancy(); + expect(form.value.isUrinePregTest).toBeNull(); + component.checkWithPregnantTimes(); + expect(form.value.noOfLivingChild).toBeNull(); + expect(form.value.isAbortion).toBeNull(); + }); + + describe('checkNoOfChildren', () => { + it('keeps valid value', () => { + form.patchValue({ noOfLivingChild: 3 }); + component.checkNoOfChildren(3); + expect(form.value.noOfLivingChild).toBe(3); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + it('rejects more than 15', () => { + form.patchValue({ noOfLivingChild: 16 }); + component.checkNoOfChildren(16); + expect(form.value.noOfLivingChild).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.childObstericHistory); + }); + it('clears zero', () => { + form.patchValue({ noOfLivingChild: 0 }); + component.checkNoOfChildren(0); + expect(form.value.noOfLivingChild).toBeNull(); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + }); + + describe('checkLengthMenstrualCycle', () => { + it('accepts 25-40 silently', () => { + component.checkLengthMenstrualCycle(ev(30)); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + it('warns for out-of-range', () => { + component.checkLengthMenstrualCycle(ev(20)); + component.checkLengthMenstrualCycle(ev(45)); + expect(confirm.alert).toHaveBeenCalledWith(info.recheckValue); + expect(confirm.alert).toHaveBeenCalledTimes(2); + }); + it('rejects non-positive and clears', () => { + form.patchValue({ menstrualCycleLength: 0 }); + component.checkLengthMenstrualCycle(ev(0)); + expect(confirm.alert).toHaveBeenCalledWith(info.invalidLength); + expect(form.value.menstrualCycleLength).toBeNull(); + }); + it('handles missing target', () => { + component.checkLengthMenstrualCycle({}); + expect(confirm.alert).toHaveBeenCalledWith(info.invalidLength); + }); + }); + + describe('checkMenstrualFlowDuration', () => { + it('accepts <= 12 silently', () => { + component.checkMenstrualFlowDuration(ev(5)); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + it('warns above 12', () => { + component.checkMenstrualFlowDuration(ev(13)); + expect(confirm.alert).toHaveBeenCalledWith(info.recheckValue); + }); + it('rejects non-positive and clears', () => { + form.patchValue({ menstrualFlowDuration: 0 }); + component.checkMenstrualFlowDuration(ev(0)); + expect(confirm.alert).toHaveBeenCalledWith(info.invalidLength); + expect(form.value.menstrualFlowDuration).toBeNull(); + }); + }); + + describe('checkMenarcheAge', () => { + beforeEach(() => ben$.next({ ageVal: 30 })); + it('accepts 8-20 silently', () => { + component.checkMenarcheAge(ev(12)); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + it('warns when outside 8-20 but <= age', () => { + component.checkMenarcheAge(ev(25)); + expect(confirm.alert).toHaveBeenCalledWith(info.recheckValue); + }); + it('rejects age above patient age', () => { + form.patchValue({ menarche_Age: 40 }); + component.checkMenarcheAge(ev(40)); + expect(form.value.menarche_Age).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.menarchAge); + }); + it('rejects zero', () => { + form.patchValue({ menarche_Age: 0 }); + component.checkMenarcheAge(ev(0)); + expect(form.value.menarche_Age).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.invalidLength); + }); + }); + + describe('checkMenopauseAge', () => { + beforeEach(() => ben$.next({ ageVal: 70 })); + it('accepts 50-60 silently', () => { + form.patchValue({ isPostMenopauseBleeding: 'Y' }); + component.checkMenopauseAge(ev(55)); + expect(form.value.isPostMenopauseBleeding).toBe('Y'); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + it('warns outside 50-60 and clears bleeding', () => { + form.patchValue({ isPostMenopauseBleeding: 'Y' }); + component.checkMenopauseAge(ev(45)); + expect(form.value.isPostMenopauseBleeding).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.recheckValue); + }); + it('rejects above patient age', () => { + form.patchValue({ menopauseAge: 80, isPostMenopauseBleeding: 'Y' }); + component.checkMenopauseAge(ev(80)); + expect(form.value.menopauseAge).toBeNull(); + expect(form.value.isPostMenopauseBleeding).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.menopauseAge); + }); + it('rejects zero', () => { + form.patchValue({ menopauseAge: 0, isPostMenopauseBleeding: 'Y' }); + component.checkMenopauseAge(ev(0)); + expect(component.menopauseAge).toBeNull(); + expect(form.value.isPostMenopauseBleeding).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith(info.invalidLength); + }); + }); + + it('pregnant_No getter reads control', () => { + form.patchValue({ pregnant_No: '2' }); + expect(component.pregnant_No).toBe('2'); + }); + + describe('getPreviousCancerPastObstetricHistory', () => { + it('opens dialog when data exists', () => { + const payload = { data: [{ x: 1 }] }; + nurse.getPreviousCancerPastObstetricHistory.and.returnValue( + of({ data: payload }) + ); + component.getPreviousCancerPastObstetricHistory(); + expect(nurse.getPreviousCancerPastObstetricHistory).toHaveBeenCalledWith( + '9' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { dataList: payload, title: LANGUAGE_EN.common.prevObsteric }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousCancerPastObstetricHistory.and.returnValue( + of({ data: { data: [] } }) + ); + component.getPreviousCancerPastObstetricHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.previousHistoryDetailsAlert.prevHabitDiet + ); + }); + it('alerts error message when data null', () => { + nurse.getPreviousCancerPastObstetricHistory.and.returnValue( + of({ data: null, errorMessage: 'e' }) + ); + component.getPreviousCancerPastObstetricHistory(); + expect(confirm.alert).toHaveBeenCalledWith('e', 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousCancerPastObstetricHistory.and.returnValue( + throwingObs('x') + ); + component.getPreviousCancerPastObstetricHistory(); + expect(confirm.alert).toHaveBeenCalledWith('x', 'error'); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/cancer-history/personal-history/personal-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/cancer-history/personal-history/personal-history.component.spec.ts index 29c366a0..dfe49bb7 100644 --- a/src/app/app-modules/nurse-doctor/history/cancer-history/personal-history/personal-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/cancer-history/personal-history/personal-history.component.spec.ts @@ -20,27 +20,408 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { MasterdataService, NurseService } from '../../../shared/services'; import { PersonalHistoryComponent } from './personal-history.component'; -describe('PersonalHistoryComponent', () => { +const CONTROLS = [ + 'tobaccoUse', + 'startAge_year', + 'endAge_year', + 'typeOfTobaccoProductList', + 'quantityPerDay', + 'isFilteredCigaerette', + 'isCigaretteExposure', + 'isBetelNutChewing', + 'durationOfBetelQuid', + 'alcoholUse', + 'ssAlcoholUsed', + 'frequencyOfAlcoholUsed', + 'dietType', + 'intakeOfOutsidePreparedMeal', + 'fruitConsumptionDays', + 'fruitQuantityPerDay', + 'vegetableConsumptionDays', + 'vegetableQuantityPerDay', + 'typeOfOilConsumedList', +]; + +function buildForm(): FormGroup { + const g: any = {}; + CONTROLS.forEach(c => (g[c] = new FormControl(null))); + return new FormGroup(g); +} + +describe('Cancer PersonalHistoryComponent', () => { let component: PersonalHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PersonalHistoryComponent], - }).compileComponents(); - })); + const master = { + alcoholUseStatus: ['a'], + dietTypes: ['d'], + oilConsumed: [ + { habitValue: 'Mustard' }, + { habitValue: 'Don’t know.' }, + { habitValue: 'Sunflower' }, + ], + physicalActivityType: ['p'], + tobaccoUseStatus: ['t'], + typeOfTobaccoProducts: ['Beedi', 'Cigarettes'], + frequencyOfAlcoholIntake: ['f'], + }; - beforeEach(() => { + beforeEach(async () => { + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PersonalHistoryComponent], + providers: [ + ...commonTestProviders({ session: { beneficiaryRegID: '77' } }), + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PersonalHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(PersonalHistoryComponent); component = fixture.componentInstance; + form = buildForm(); + component.cancerPatientPerosnalHistoryForm = form; fixture.detectChanges(); }); - it('should create', () => { + it('creates and loads language on init/DoCheck', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ignores null master data and beneficiary', () => { + expect(component.templateNurseMasterData).toBeUndefined(); + expect(component.templateBeneficiaryDetails).toBeUndefined(); + }); + + it('maps nurse master data', () => { + master$.next(master); + expect(component.templateNurseMasterData).toBe(master); + expect(component.templateAlcoholUseStatus).toBe(master.alcoholUseStatus); + expect(component.templateDietTypes).toBe(master.dietTypes); + expect(component.templateOilConsumed).toBe(master.oilConsumed); + expect(component.filteredOilConsumed).toBe(master.oilConsumed); + expect(component.templatePhysicalActivityType).toBe( + master.physicalActivityType + ); + expect(component.templateTobaccoUseStatus).toBe(master.tobaccoUseStatus); + expect(component.templateTobaccoProductUsed).toBe( + master.typeOfTobaccoProducts + ); + expect(component.templateFrequecyOfAlcohol).toBe( + master.frequencyOfAlcoholIntake + ); + }); + + it('stores beneficiary details and age', () => { + ben$.next({ ageVal: 40 }); + expect(component.templateBeneficiaryDetails).toEqual({ ageVal: 40 }); + expect(component.patientAge).toBe(40); + }); + + it('ngOnDestroy unsubscribes both subscriptions', () => { + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.beneficiaryDetailSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + }); + + it('ngOnDestroy tolerates missing subscriptions', () => { + component.nurseMasterDataSubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + describe('tobacco product selection (valueChanges)', () => { + it('flags beedi and cigarettes', () => { + form.patchValue({ typeOfTobaccoProductList: ['Beedi', 'Cigarettes'] }); + expect(component.doSmokeBeedi).toBeTrue(); + expect(component.doSmokeCigarettes).toBeTrue(); + }); + + it('clears filtered cigarette when cigarettes not chosen', () => { + form.patchValue({ isFilteredCigaerette: 'yes' }); + form.patchValue({ typeOfTobaccoProductList: ['Beedi'] }); + expect(component.doSmokeBeedi).toBeTrue(); + expect(component.doSmokeCigarettes).toBeFalse(); + expect(form.value.isFilteredCigaerette).toBeNull(); + }); + + it('handles an empty list', () => { + form.patchValue({ typeOfTobaccoProductList: null }); + expect(component.doSmokeBeedi).toBeFalse(); + expect(component.doSmokeCigarettes).toBeFalse(); + }); + }); + + it('checkTobaccoUseStatus resets ages/products only when value given', () => { + form.patchValue({ + startAge_year: 5, + endAge_year: 6, + typeOfTobaccoProductList: ['Beedi'], + }); + component.checkTobaccoUseStatus(null); + expect(form.value.startAge_year).toBe(5); + component.checkTobaccoUseStatus('Current'); + expect(form.value.startAge_year).toBeNull(); + expect(form.value.endAge_year).toBeNull(); + expect(form.value.typeOfTobaccoProductList).toBeNull(); + }); + + it('reset helpers clear their dependent fields', () => { + form.patchValue({ + durationOfBetelQuid: 3, + ssAlcoholUsed: 'x', + frequencyOfAlcoholUsed: 'y', + fruitQuantityPerDay: 2, + vegetableQuantityPerDay: 2, + }); + component.checkBetelQuid(); + expect(form.value.durationOfBetelQuid).toBeNull(); + component.checkUsageOfAlcohol(); + expect(form.value.ssAlcoholUsed).toBeNull(); + expect(form.value.frequencyOfAlcoholUsed).toBeNull(); + form.patchValue({ frequencyOfAlcoholUsed: 'z' }); + component.checkLastAlcoholUsage(); + expect(form.value.frequencyOfAlcoholUsed).toBeNull(); + component.checkFruitConsumption(); + expect(form.value.fruitQuantityPerDay).toBeNull(); + component.checkVegetableConsumption(); + expect(form.value.vegetableQuantityPerDay).toBeNull(); + }); + + it('getters return control values', () => { + form.patchValue({ + tobaccoUse: 'T', + isBetelNutChewing: true, + ssAlcoholUsed: 'S', + alcoholUse: 'A', + dietType: 'D', + fruitConsumptionDays: 3, + vegetableConsumptionDays: 4, + typeOfOilConsumedList: ['O'], + }); + expect(component.tobaccoUse).toBe('T'); + expect(component.isBetelNutChewing).toBeTrue(); + expect(component.ssAlcoholUsed).toBe('S'); + expect(component.alcoholUse).toBe('A'); + expect(component.dietType).toBe('D'); + expect(component.fruitConsumptionDays).toBe(3); + expect(component.vegetableConsumptionDays).toBe(4); + expect(component.typeOfOilConsumedList).toEqual(['O']); + }); + + describe('oilSelected', () => { + beforeEach(() => master$.next(master)); + + it("keeps only Don't know when it is selected", () => { + form.patchValue({ typeOfOilConsumedList: ['Don’t know.'] }); + component.oilSelected(); + expect(component.templateOilConsumed).toEqual([ + { habitValue: 'Don’t know.' }, + ]); + }); + + it("removes Don't know when other oils selected", () => { + form.patchValue({ typeOfOilConsumedList: ['Mustard'] }); + component.oilSelected(); + expect(component.templateOilConsumed.length).toBe(2); + }); + + it('restores full list when nothing selected', () => { + form.patchValue({ typeOfOilConsumedList: ['Mustard'] }); + component.oilSelected(); + form.patchValue({ typeOfOilConsumedList: [] }); + component.oilSelected(); + expect(component.templateOilConsumed).toBe(master.oilConsumed); + }); + }); + + describe('prevent consumption keys', () => { + ['preventFruitConsumption', 'preventVegetableConsumption'].forEach(m => { + it(`${m} blocks keys > 7`, () => { + const ev = { key: '8', preventDefault: jasmine.createSpy('pd') }; + (component as any)[m](ev); + expect(ev.preventDefault).toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.daysInWeek + ); + }); + + it(`${m} allows keys <= 7`, () => { + const ev = { key: '5', preventDefault: jasmine.createSpy('pd') }; + (component as any)[m](ev); + expect(ev.preventDefault).not.toHaveBeenCalled(); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + }); + }); + + describe('age checks', () => { + beforeEach(() => ben$.next({ ageVal: 30 })); + const ev = (v: any) => ({ target: { value: v } }); + + it('checkStartAge rejects age above patient age', () => { + form.patchValue({ startAge_year: 40 }); + component.checkStartAge(ev('40')); + expect(component.startAgeYear).toBe(40); + expect(form.value.startAge_year).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.startAge + ); + }); + + it('checkStartAge rejects zero', () => { + form.patchValue({ startAge_year: 0 }); + component.checkStartAge(ev('0')); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.invalidLessThan + ); + }); + + it('checkStartAge accepts a valid age', () => { + form.patchValue({ startAge_year: 10 }); + component.checkStartAge(ev('10')); + expect(form.value.startAge_year).toBe(10); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + + it('checkStopAge rejects age above patient age', () => { + form.patchValue({ endAge_year: 50 }); + component.checkStopAge(ev('50')); + expect(form.value.endAge_year).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.stopAge + ); + }); + + it('checkStopAge rejects age lower than start age', () => { + component.startAgeYear = 20; + component.checkStopAge(ev('10')); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.ageLess + ); + }); + + it('checkStopAge rejects zero', () => { + component.checkStopAge(ev('0')); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.invalidLessThan + ); + }); + + it('checkStopAge accepts a valid age', () => { + component.startAgeYear = 5; + form.patchValue({ endAge_year: 10 }); + component.checkStopAge(ev('10')); + expect(form.value.endAge_year).toBe(10); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + }); + + it('fruit/vegetable per-day checks reject zero only', () => { + form.patchValue({ fruitQuantityPerDay: 2, vegetableQuantityPerDay: 2 }); + component.fruitConsumptionPerDayCheck({ target: { value: '2' } }); + component.vegetableConsumptionPerDayCheck({ target: { value: '2' } }); + expect(confirm.alert).not.toHaveBeenCalled(); + component.fruitConsumptionPerDayCheck({ target: { value: '0' } }); + expect(form.value.fruitQuantityPerDay).toBeNull(); + component.vegetableConsumptionPerDayCheck({ target: { value: '0' } }); + expect(form.value.vegetableQuantityPerDay).toBeNull(); + expect(confirm.alert).toHaveBeenCalledTimes(2); + }); + + it('minimumvaluenonZeroBetelQuid clears only on numeric zero', () => { + form.patchValue({ durationOfBetelQuid: 4 }); + component.minimumvaluenonZeroBetelQuid({ target: { value: 3 } }); + expect(form.value.durationOfBetelQuid).toBe(4); + component.minimumvaluenonZeroBetelQuid({ target: { value: 0 } }); + expect(form.value.durationOfBetelQuid).toBeNull(); + }); + + [ + 'getPreviousCancerPersonalHabitHistory', + 'getPreviousCancerPersonalDietHistory', + ].forEach(method => { + describe(method, () => { + it('opens previous-details dialog when data exists', () => { + const payload = { data: [{ a: 1 }] }; + nurse[method].and.returnValue(of({ data: payload })); + (component as any)[method](); + expect(nurse[method]).toHaveBeenCalledWith('77'); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.personalhistory.previouspersonalhistory, + }, + }); + }); + + it('alerts when history is empty', () => { + nurse[method].and.returnValue(of({ data: { data: [] } })); + (component as any)[method](); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.previousHistoryDetailsAlert.prevHabitDiet + ); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + it('alerts error when data is null', () => { + nurse[method].and.returnValue(of({ data: null, errorMessage: 'bad' })); + (component as any)[method](); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + }); + + it('alerts on request failure', () => { + nurse[method].and.returnValue(throwingObs('boom')); + (component as any)[method](); + expect(confirm.alert).toHaveBeenCalledWith('boom', 'error'); + }); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/comorbidity-concurrent-conditions/comorbidity-concurrent-conditions.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/comorbidity-concurrent-conditions/comorbidity-concurrent-conditions.component.spec.ts index 6d070988..2073d162 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/comorbidity-concurrent-conditions/comorbidity-concurrent-conditions.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/comorbidity-concurrent-conditions/comorbidity-concurrent-conditions.component.spec.ts @@ -20,129 +20,462 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MaterialModule } from 'src/app/app-modules/core/material.module'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { PreviousDetailsComponent } from '../../../../core/components/previous-details/previous-details.component'; +import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; import { ConfirmationService } from '../../../../core/services/confirmation.service'; import { DoctorService, - NurseService, MasterdataService, + NurseService, } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { NurseServiceStub } from '../../../shared/mocks/nurse-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { ComorbidityConcurrentConditionsComponent } from './comorbidity-concurrent-conditions.component'; describe('ComorbidityConcurrentConditionsComponent', () => { let component: ComorbidityConcurrentConditionsComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let session: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - schemas: [NO_ERRORS_SCHEMA], + const master = () => [ + { comorbidCondition: 'Asthma' }, + { comorbidCondition: 'Diabetes' }, + { comorbidCondition: 'Nil' }, + { comorbidCondition: 'None' }, + { comorbidCondition: 'Other' }, + ]; + + const makeForm = () => + new FormGroup({ comorbidityConcurrentConditionsList: new FormArray([]) }); + + const list = () => + component.comorbidityConcurrentConditionsForm.controls[ + 'comorbidityConcurrentConditionsList' + ] as FormArray; + + async function setup(session0: Record = {}) { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject({ age: '30 Years' }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [ComorbidityConcurrentConditionsComponent], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1', ...session0 }, + }), + FormBuilder, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), }, - { provide: NurseService, useClass: NurseServiceStub }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ComorbidityConcurrentConditionsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + component.comorbidityConcurrentConditionsForm = makeForm(); + } - fb = debugElement.injector.get(FormBuilder); - component.comorbidityConcurrentConditionsForm = new GeneralUtils( - fb - ).createComorbidityConcurrentConditionsForm(); - window.console.log = () => {}; + describe('default', () => { + beforeEach(async () => { + await setup(); + fixture.detectChanges(); + }); - fixture.detectChanges(); - }); + afterEach(() => component.ngOnDestroy()); + + it('initialises language, beneficiary and filter status', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.beneficiary).toEqual({ age: '30 Years' }); + expect(component.ComorbidStatus).toBe('false'); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('does nothing while master data is not loaded', () => { + expect(list().length).toBe(0); + expect(component.comorbidityMasterData).toBeUndefined(); + }); + + it('adds first row with filtered master list once master data arrives', () => { + master$.next({ comorbidConditions: master() }); + fixture.detectChanges(); + expect(list().length).toBe(1); + expect(component.comorbiditySelectList.length).toBe(1); + expect(component.comorbiditySelectList[0].length).toBe(5); + expect(list().at(0).get('timePeriodAgo')?.disabled).toBeTrue(); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + const rows = fixture.nativeElement.querySelectorAll('.multi_row_select'); + expect(rows.length).toBe(1); + }); + + it('getcomorbidityConcurrentConditions returns controls or null', () => { + master$.next({ comorbidConditions: master() }); + expect(component.getcomorbidityConcurrentConditions()?.length).toBe(1); + component.comorbidityConcurrentConditionsForm = new FormGroup({}); + expect(component.getcomorbidityConcurrentConditions()).toBeNull(); + }); + + it('addComorbidityConcurrentConditions excludes already selected and None/Nil', () => { + master$.next({ comorbidConditions: master() }); + const m = component.comorbidityMasterData; + list().at(0).patchValue({ comorbidConditions: m[0] }); + component.addComorbidityConcurrentConditions(); + const names = component.comorbiditySelectList[1].map( + (x: any) => x.comorbidCondition + ); + expect(names).toEqual(['Diabetes', 'Other']); + // "Other" selections are not excluded + list().at(1).patchValue({ comorbidConditions: m[4] }); + component.addComorbidityConcurrentConditions(); + expect( + component.comorbiditySelectList[2].map((x: any) => x.comorbidCondition) + ).toEqual(['Diabetes', 'Other']); + }); + + it('addComorbidityConcurrentConditions without master data only pushes a row', () => { + component.comorbidityFilteredMasterData = undefined; + component.addComorbidityConcurrentConditions(); + expect(list().length).toBe(1); + expect(component.comorbiditySelectList.length).toBe(0); + }); + + it('filter selection enables fields and removes item from other lists', () => { + master$.next({ comorbidConditions: master() }); + component.addComorbidityConcurrentConditions(); + const m = component.comorbidityMasterData; + const row0 = list().at(0); + row0.patchValue({ otherComorbidCondition: 'x' }); + expect(component.comorbiditySelectList[1]).toContain(m[0]); + component.filterComorbidityConcurrentConditionsType(m[0], 0, row0); + expect(row0.value.otherComorbidCondition).toBeNull(); + expect(row0.get('timePeriodAgo')?.enabled).toBeTrue(); + expect(row0.get('isForHistory')?.enabled).toBeTrue(); + expect(component.previousSelectedComorbidity[0]).toBe(m[0]); + expect(component.comorbiditySelectList[1]).not.toContain(m[0]); + + // change selection: previous value returned to other lists, sorted + component.filterComorbidityConcurrentConditionsType(m[1], 0, row0); + expect(component.comorbiditySelectList[1][0]).toBe(m[0]); + expect(component.previousSelectedComorbidity[0]).toBe(m[1]); + }); + + it('filter selection of Other keeps other-text and is not removed elsewhere', () => { + master$.next({ comorbidConditions: master() }); + component.addComorbidityConcurrentConditions(); + const m = component.comorbidityMasterData; + const row0 = list().at(0); + row0.patchValue({ otherComorbidCondition: 'custom' }); + component.filterComorbidityConcurrentConditionsType(m[4], 0, row0); + expect(row0.value.otherComorbidCondition).toBe('custom'); + expect(component.comorbiditySelectList[1]).toContain(m[4]); + // switching away from previous Other does not push it back + const lenBefore = component.comorbiditySelectList[1].length; + component.filterComorbidityConcurrentConditionsType(m[1], 0, row0); + expect(component.comorbiditySelectList[1].length).toBe(lenBefore - 1); + }); + + it('selecting None/Nil removes other rows and disables duration fields', () => { + master$.next({ comorbidConditions: master() }); + const m = component.comorbidityMasterData; + component.addComorbidityConcurrentConditions(); + component.addComorbidityConcurrentConditions(); + component.previousSelectedComorbidity = [undefined, m[1], undefined]; + const row0 = list().at(0); + row0.get('timePeriodAgo')?.enable(); + component.filterComorbidityConcurrentConditionsType(m[3], 0, row0); + expect(list().length).toBe(1); + expect(component.comorbiditySelectList.length).toBe(1); + expect(row0.get('timePeriodAgo')?.disabled).toBeTrue(); + expect(row0.get('timePeriodUnit')?.disabled).toBeTrue(); + expect(row0.get('isForHistory')?.disabled).toBeTrue(); + component.filterComorbidityConcurrentConditionsType(m[2], 0); + expect(component.previousSelectedComorbidity[0]).toBe(m[2]); + }); - it('should create', () => { - expect(component).toBeTruthy(); + it('remove row: cancel keeps row', () => { + master$.next({ comorbidConditions: master() }); + confirm.confirm.and.returnValue(of(false)); + component.removeComorbidityConcurrentConditions(0, list().at(0)); + expect(list().length).toBe(1); + expect(component.comorbidityConcurrentConditionsForm.dirty).toBeFalse(); + }); + + it('remove last remaining row resets its values', () => { + master$.next({ comorbidConditions: master() }); + const row0 = list().at(0); + row0.patchValue({ + comorbidConditions: master()[0], + otherComorbidCondition: 'a', + }); + row0.get('timePeriodAgo')?.enable(); + row0.get('timePeriodUnit')?.enable(); + component.removeComorbidityConcurrentConditions(0, row0); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(list().length).toBe(1); + expect(row0.value.comorbidConditions).toBeNull(); + expect(row0.get('timePeriodAgo')?.disabled).toBeTrue(); + expect(row0.get('timePeriodUnit')?.disabled).toBeTrue(); + expect(component.comorbidityConcurrentConditionsForm.dirty).toBeTrue(); + }); + + it('remove a row among many returns its value to other lists', () => { + master$.next({ comorbidConditions: master() }); + const m = component.comorbidityMasterData; + component.addComorbidityConcurrentConditions(); + component.previousSelectedComorbidity = [m[0], m[1]]; + component.comorbiditySelectList = [[m[0]], [m[1]]]; + component.removeComorbidityConcurrentConditions(1, list().at(1)); + expect(list().length).toBe(1); + expect(component.comorbiditySelectList).toEqual([[m[0], m[1]]]); + expect(component.previousSelectedComorbidity).toEqual([m[0]]); + }); + + it('remove a row whose value was Other does not return it', () => { + master$.next({ comorbidConditions: master() }); + const m = component.comorbidityMasterData; + component.addComorbidityConcurrentConditions(); + component.previousSelectedComorbidity = [m[0], m[4]]; + component.comorbiditySelectList = [[m[0]], [m[4]]]; + component.removeComorbidityConcurrentConditions(1); + expect(component.comorbiditySelectList).toEqual([[m[0]]]); + }); + + it('validateDuration alerts and clears when duration exceeds age', () => { + const g: any = component.initComorbidityConcurrentConditions(); + g.get('timePeriodAgo')?.enable(); + g.get('timePeriodUnit')?.enable(); + g.patchValue({ timePeriodAgo: 40, timePeriodUnit: 'Years' }); + component.validateDuration(g); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.durationGreaterThanAge + ); + expect(g.get('timePeriodAgo')?.value).toBeNull(); + expect(g.get('timePeriodUnit')?.value).toBeNull(); + }); + + it('validateDuration valid duration keeps value', () => { + const g: any = component.initComorbidityConcurrentConditions(); + g.get('timePeriodAgo')?.enable(); + g.get('timePeriodUnit')?.enable(); + g.patchValue({ timePeriodAgo: 2, timePeriodUnit: 'Years' }); + component.validateDuration(g); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(g.get('timePeriodAgo')?.value).toBe(2); + expect(g.get('timePeriodUnit')?.value).toBe('Years'); + }); + + it('validateDuration enables unit when only duration is given', () => { + const g: any = component.initComorbidityConcurrentConditions(); + g.get('timePeriodAgo')?.enable(); + g.patchValue({ timePeriodAgo: 3 }); + component.validateDuration(g); + expect(g.get('timePeriodUnit')?.enabled).toBeTrue(); + expect(g.get('timePeriodUnit')?.value).toBeNull(); + }); + + it('sortComorbidityList sorts alphabetically', () => { + const arr = [ + { comorbidCondition: 'b' }, + { comorbidCondition: 'a' }, + { comorbidCondition: 'b' }, + ]; + component.sortComorbidityList(arr); + expect(arr.map(a => a.comorbidCondition)).toEqual(['a', 'b', 'b']); + }); + + it('checkValidity is false only when all three fields are set', () => { + const g: any = component.initComorbidityConcurrentConditions(); + expect(component.checkValidity(g)).toBeTrue(); + g.get('timePeriodAgo')?.enable(); + g.get('timePeriodUnit')?.enable(); + g.patchValue({ + comorbidConditions: { comorbidCondition: 'Asthma' }, + timePeriodAgo: 1, + timePeriodUnit: 'Days', + }); + expect(component.checkValidity(g)).toBeFalse(); + }); + + it('getPreviousComorbidityHistory opens dialog with data', () => { + component.visitCategory = 'General OPD'; + const data = { data: [{ a: 1 }], columns: [] }; + nurse.getPreviousComorbidityHistory.and.returnValue( + of({ statusCode: 200, data }) + ); + component.getPreviousComorbidityHistory(); + expect(nurse.getPreviousComorbidityHistory).toHaveBeenCalledWith( + 'b1', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: data, + title: + LANGUAGE_EN.historyData.comorbiditycondition + .previouscomorbidityhistory, + }, + }); + }); + + it('getPreviousComorbidityHistory alerts when empty, failed or errored', () => { + nurse.getPreviousComorbidityHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousComorbidityHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + nurse.getPreviousComorbidityHistory.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getPreviousComorbidityHistory(); + nurse.getPreviousComorbidityHistory.and.returnValue(throwingObs()); + component.getPreviousComorbidityHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + expect(confirm.alert).toHaveBeenCalledTimes(3); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + it('onComorbidFilterClick sets status for COVID screening', () => { + session.setItem('visiCategoryANC', 'COVID-19 Screening'); + component.onComorbidFilterClick(); + expect(component.ComorbidStatus).toBe('true'); + session.setItem('visiCategoryANC', 'ANC'); + component.onComorbidFilterClick(); + expect(component.ComorbidStatus).toBe('false'); + }); + + it('ngOnDestroy unsubscribes all subscriptions', () => { + component.getGeneralHistory('b1', 'v1'); + const subs = [ + component.nurseMasterDataSubscription, + component.generalHistorySubscription, + component.beneficiaryDetailSubscription, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + }); + + it('ngOnDestroy tolerates missing subscriptions', () => { + component.nurseMasterDataSubscription = undefined; + component.generalHistorySubscription = undefined; + component.beneficiaryDetailSubscription = undefined; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); - it('should call getBeneficiaryDetails on Initialisation', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); - expect(component.beneficiary).toEqual(data.femaleBeneficiary); - } - )); - - it('should call getMasterData on Initialisation', inject( - [MasterdataService], - masterdataService => { - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data + describe('view mode', () => { + beforeEach(async () => { + await setup(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('loads and patches saved comorbidity history', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + ComorbidityConditions: { + comorbidityConcurrentConditionsList: [ + { + comorbidCondition: 'Asthma', + timePeriodAgo: 2, + timePeriodUnit: 'Years', + isForHistory: true, + }, + { + comorbidCondition: 'Unknown', + timePeriodAgo: null, + timePeriodUnit: null, + }, + { comorbidCondition: null }, + ], + }, + }, + }) ); - spyOn(component, 'getMasterData'); - component.ngOnInit(); - expect(component.getMasterData).toHaveBeenCalled(); - expect(component.comorbidityMasterData).toEqual( - data.generalOPDNurseMasterdata.data.comorbidConditions + component.mode = 'view'; + fixture.detectChanges(); + master$.next({ comorbidConditions: master() }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(list().length).toBe(3); + const row0 = list().at(0); + expect(row0.value.comorbidConditions.comorbidCondition).toBe('Asthma'); + expect(row0.get('timePeriodAgo')?.enabled).toBeTrue(); + expect(row0.get('timePeriodUnit')?.enabled).toBeTrue(); + expect(row0.dirty).toBeTrue(); + expect(list().at(1).get('timePeriodUnit')?.disabled).toBeTrue(); + expect(list().at(2).dirty).toBeFalse(); + }); + + it('ignores empty or failed history responses', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) ); - } - )); - - // it('should intialise comorbidity FormArray with comorbidity FormGroup on Initialisation', inject([MasterdataService], (masterdataService) => { - // masterdataService.nurseMasterDataSource.next(data.generalOPDNurseMasterdata.data); - // spyOn(component, 'getMasterData').and.callThrough(); - // spyOn(component, 'addComorbidityConcurrentConditions'); - // component.ngOnInit(); - // expect(component.getMasterData).toHaveBeenCalled(); - // expect(component.addComorbidityConcurrentConditions).toHaveBeenCalled(); - // expect((component.comorbidityConcurrentConditionsForm.controls.comorbidityConcurrentConditionsList).controls.length).toBe(1); - // })); - - // it('should call getGeneralHistory when mode is view', async(inject([MasterdataService], (masterdataService) => { - // masterdataService.nurseMasterDataSource.next(data.generalOPDNurseMasterdata.data); - // spyOn(component, 'getMasterData').and.callThrough(); - // spyOn(component, 'getGeneralHistory'); - // component.mode = "view"; - // fixture.detectChanges(); - // component.ngOnInit(); - // expect(component.getMasterData).toHaveBeenCalled(); - // expect(component.getGeneralHistory).toHaveBeenCalled(); - // }))); + component.mode = 'view'; + fixture.detectChanges(); + master$.next({ comorbidConditions: master() }); + expect(component.comorbidtyData).toBeUndefined(); + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('b1', 'v1'); + expect(component.comorbidtyData).toBeUndefined(); + expect(list().length).toBe(1); + }); + }); + + describe('COVID screening', () => { + beforeEach(async () => { + await setup({ visiCategoryANC: 'COVID-19 Screening' }); + fixture.detectChanges(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('marks comorbid condition as required', () => { + expect(component.ComorbidStatus).toBe('true'); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/development-history/development-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/development-history/development-history.component.spec.ts index 3443913b..ac7b4c86 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/development-history/development-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/development-history/development-history.component.spec.ts @@ -20,76 +20,194 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { DoctorService, - NurseService, MasterdataService, + NurseService, } from '../../../shared/services'; - -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; -import { NurseServiceStub } from '../../../shared/mocks/nurse-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { DevelopmentHistoryComponent } from './development-history.component'; describe('DevelopmentHistoryComponent', () => { let component: DevelopmentHistoryComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - schemas: [NO_ERRORS_SCHEMA], + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [DevelopmentHistoryComponent], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, - { provide: NurseService, useClass: NurseServiceStub }, + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(DevelopmentHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + spyOn(console, 'log'); fixture = TestBed.createComponent(DevelopmentHistoryComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + form = new FormGroup({ + grossMotorMilestones: new FormControl(null), + isGrossMotorMilestones: new FormControl(null), + }); + component.developmentHistoryForm = form; + component.mode = mode; + component.visitCategory = 'General OPD'; + fixture.detectChanges(); + } - fb = debugElement.injector.get(FormBuilder); - component.developmentHistoryForm = new GeneralUtils( - fb - ).createDevelopmentHistoryForm(); - window.console.log = () => {}; + describe('new mode', () => { + beforeEach(async () => setup('new')); - fixture.detectChanges(); + it('sets language and stores master data without loading history', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.masterData).toBeUndefined(); + const m = { a: 1 }; + master$.next(m); + expect(component.masterData).toBe(m); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ngOnDestroy unsubscribes when present and tolerates nulls', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.generalHistorySubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + describe('getGeneralHistory', () => { + it('patches form with DevelopmentHistory', () => { + const dh = { grossMotorMilestones: 'x', isGrossMotorMilestones: true }; + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { DevelopmentHistory: dh } }) + ); + component.getGeneralHistory('1', '2'); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('1', '2'); + expect(component.developmentHistoryData).toBe(dh); + expect(form.value).toEqual(dh); + }); + + [ + null, + { statusCode: 500, data: { DevelopmentHistory: {} } }, + { statusCode: 200, data: null }, + { statusCode: 200, data: {} }, + ].forEach((resp, i) => { + it(`ignores unusable response #${i}`, () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(resp)); + component.getGeneralHistory('1', '2'); + expect(component.developmentHistoryData).toBeUndefined(); + expect(form.value.grossMotorMilestones).toBeNull(); + }); + }); + }); + + describe('getPreviousDevelopmentalHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousDevelopmentalHistory.and.returnValue( + of({ data: payload }) + ); + component.getPreviousDevelopmentalHistory(); + expect(nurse.getPreviousDevelopmentalHistory).toHaveBeenCalledWith( + '11', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.Perinatalhistorydetails + .developmentalhistorydetails, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousDevelopmentalHistory.and.returnValue( + of({ data: { data: [] } }) + ); + component.getPreviousDevelopmentalHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + expect(dialog.open).not.toHaveBeenCalled(); + }); + it('alerts when data null', () => { + nurse.getPreviousDevelopmentalHistory.and.returnValue( + of({ data: null }) + ); + component.getPreviousDevelopmentalHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts when response null', () => { + nurse.getPreviousDevelopmentalHistory.and.returnValue(of(null)); + component.getPreviousDevelopmentalHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousDevelopmentalHistory.and.returnValue(throwingObs()); + component.getPreviousDevelopmentalHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('loads history with session IDs when master data arrives', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { DevelopmentHistory: { grossMotorMilestones: 'g' } }, + }) + ); + master$.next({ a: 1 }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.grossMotorMilestones).toBe('g'); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history-ncdscreening/family-history-ncdscreening.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history-ncdscreening/family-history-ncdscreening.component.spec.ts index db8d8e18..cf66013c 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history-ncdscreening/family-history-ncdscreening.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history-ncdscreening/family-history-ncdscreening.component.spec.ts @@ -20,27 +20,501 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MaterialModule } from 'src/app/app-modules/core/material.module'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { PreviousDetailsComponent } from '../../../../core/components/previous-details/previous-details.component'; +import { + BeneficiaryDetailsService, + ConfirmationService, +} from 'src/app/app-modules/core/services'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; +import { IdrsscoreService } from '../../../shared/services/idrsscore.service'; import { FamilyHistoryNcdscreeningComponent } from './family-history-ncdscreening.component'; describe('FamilyHistoryNcdscreeningComponent', () => { let component: FamilyHistoryNcdscreeningComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let idrs: any; + let confirm: any; + let dialog: any; + let session: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let m: any[]; + + const diseases = () => [ + { diseaseType: 'Asthma', snomedCode: '195967001', snomedTerm: 'Asthma' }, + { diseaseType: 'Diabetes Mellitus', snomedCode: null, snomedTerm: null }, + { diseaseType: 'Nil', snomedCode: null }, + { diseaseType: 'None', snomedCode: null }, + { diseaseType: 'Other', snomedCode: null }, + ]; + const list = () => + component.familyHistoryForm.controls['familyDiseaseList'] as FormArray; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + idrs = autoSpy( + IdrsscoreService, + { + dummyValue$: new BehaviorSubject('dummy').asObservable(), + IDRSFamilyScore$: new BehaviorSubject(5).asObservable(), + }, + undefined + ); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject({ ageVal: 40 }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [FamilyHistoryNcdscreeningComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + FormBuilder, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: IdrsscoreService, useValue: idrs }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(FamilyHistoryNcdscreeningComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + component.familyHistoryForm = new FormGroup({ + familyDiseaseList: new FormArray([]), + isGeneticDisorder: new FormControl(null), + geneticDisorder: new FormControl(null), + isConsanguineousMarrige: new FormControl(null), + }); + spyOn(console, 'log'); + }); + + afterEach(() => component.ngOnDestroy()); + + describe('initialisation', () => { + it('subscribes to idrs streams, language and beneficiary', () => { + fixture.detectChanges(); + expect(idrs.clearMessage).toHaveBeenCalled(); + expect(component.dummyValue).toBe('dummy'); + expect(component.idrsscoredummy).toBe(5); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.age).toBe(40); + ben$.next({ ageVal: 0 }); + expect(component.age).toBe(0); + ben$.next({}); + expect(component.age).toBe(0); + ben$.next({ ageVal: 12 }); + ben$.next(null); + expect(component.age).toBe(12); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('adds the first disease row when master data arrives', () => { + fixture.detectChanges(); + expect(list().length).toBe(0); + master$.next({ DiseaseTypes: diseases(), familyMemberTypes: ['Father'] }); + fixture.detectChanges(); + expect(component.familyMemeberMasterData).toEqual(['Father']); + expect(list().length).toBe(1); + expect(component.diseaseSelectList[0].length).toBe(5); + expect(list().at(0).get('familyMembers')?.disabled).toBeTrue(); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + expect(component.getFamilyDiseases()?.length).toBe(1); + component.familyHistoryForm = new FormGroup({ + isGeneticDisorder: new FormControl(null), + }); + expect(component.getFamilyDiseases()).toBeNull(); + }); + }); + + describe('view mode', () => { + it('patches saved family history and computes IDRS score', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + FamilyHistory: { + familyDiseaseList: [ + { + ID: 1, + deleted: false, + diseaseType: 'Diabetes Mellitus', + familyMembers: ['Father', 'Mother'], + }, + { ID: 2, deleted: false, diseaseType: 'Unknown' }, + { ID: 3, deleted: false, diseaseType: '' }, + ], + }, + }, + }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next({ DiseaseTypes: diseases(), familyMemberTypes: [] }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(list().length).toBe(3); + const r0 = list().at(0); + expect(r0.value.diseaseType.diseaseType).toBe('Diabetes Mellitus'); + expect(r0.get('familyMembers')?.enabled).toBeTrue(); + expect(r0.dirty).toBeTrue(); + expect(list().at(1).get('familyMembers')?.enabled).toBeTrue(); + expect(list().at(2).dirty).toBeFalse(); + expect(idrs.setdymmyvalue).toHaveBeenCalledWith('Diabetes Mellitus'); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(20); + }); + + it('ignores empty or failed history', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next({ DiseaseTypes: diseases() }); + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('b1', 'v1'); + expect(component.familyHistoryData).toBeUndefined(); + expect(list().length).toBe(1); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('with master data', () => { + beforeEach(() => { + fixture.detectChanges(); + master$.next({ DiseaseTypes: diseases(), familyMemberTypes: [] }); + m = component.diseaseMasterData; + }); + + it('addFamilyDisease excludes selected and None/Nil; tolerates no master', () => { + list().at(0).patchValue({ diseaseType: m[0] }); + component.addFamilyDisease(); + expect( + component.diseaseSelectList[1].map((d: any) => d.diseaseType) + ).toEqual(['Diabetes Mellitus', 'Other']); + list().at(1).patchValue({ diseaseType: m[4] }); + component.addFamilyDisease(); + expect(component.diseaseSelectList[2].length).toBe(2); + component.diseaseMasterData = null; + component.addFamilyDisease(); + expect(component.diseaseSelectList.length).toBe(3); + expect(list().length).toBe(4); + }); + + it('addFamilyDiseaseTest re-offers deleted diseases and sorts new list', () => { + component.addFamilyDisease(); + list().at(0).patchValue({ diseaseType: m[0], deleted: true }); + list().at(1).patchValue({ diseaseType: m[1], deleted: false }); + component.addFamilyDiseaseTest(1); + expect(list().length).toBe(3); + expect( + component.diseaseSelectList[2].map((d: any) => d.diseaseType) + ).toEqual(['Asthma', 'Nil', 'None', 'Other']); + }); + + it('addFamilyDiseaseTest does not re-offer deleted disease still in use', () => { + component.addFamilyDisease(); + component.addFamilyDisease(); + list().at(0).patchValue({ diseaseType: m[0], deleted: true }); + list().at(1).patchValue({ diseaseType: m[0], deleted: false }); + list().at(2).patchValue({ diseaseType: m[4], deleted: false }); + component.addFamilyDiseaseTest(2); + expect( + component.diseaseSelectList[3].map((d: any) => d.diseaseType) + ).toEqual(['Diabetes Mellitus', 'Nil', 'None', 'Other']); + component.diseaseMasterData = null; + component.addFamilyDiseaseTest(3); + expect(list().length).toBe(5); + expect(component.diseaseSelectList.length).toBe(4); + }); + + it('filterFamilyDiseaseList with snomed code patches snomed and enables members', () => { + component.addFamilyDisease(); + const row = list().at(0); + row.patchValue({ otherDiseaseType: 'x' }); + component.filterFamilyDiseaseList(m[0], 0, row); + expect(idrs.setdymmyvalue).toHaveBeenCalledWith('Asthma'); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + expect(row.value.snomedCode).toBe('195967001'); + expect(row.value.snomedTerm).toBe('Asthma'); + expect(row.value.otherDiseaseType).toBeNull(); + expect(row.get('familyMembers')?.enabled).toBeTrue(); + expect(component.diseaseSelectList[1]).not.toContain(m[0]); + expect(component.previousSelectedDiseaseList[0]).toBe(m[0]); + }); + + it('filterFamilyDiseaseList without snomed / Other clears snomed; returns previous', () => { + component.addFamilyDisease(); + const row = list().at(0); + component.filterFamilyDiseaseList(m[0], 0, row); + row.patchValue({ diseaseType: m[1] }); + component.filterFamilyDiseaseList(m[1], 0, row); + expect(row.value.snomedCode).toBeNull(); + expect(component.diabetesPresent).toBeTrue(); + expect(component.diseaseSelectList[1]).toContain(m[0]); + row.patchValue({ diseaseType: m[4] }); + component.filterFamilyDiseaseList(m[4], 0, row); + expect(row.value.snomedTerm).toBeNull(); + expect(component.diabetesPresent).toBeFalse(); + expect(component.diseaseSelectList[1]).toContain(m[4]); + const len = component.diseaseSelectList[1].length; + component.filterFamilyDiseaseList(m[0], 0); + expect(component.diseaseSelectList[1].length).toBe(len - 1); + }); + + it('filterFamilyDiseaseList None/Nil removes other rows and disables members', () => { + component.addFamilyDisease(); + component.addFamilyDisease(); + component.previousSelectedDiseaseList = [undefined, m[0], undefined]; + const row = list().at(0); + row.get('familyMembers')?.enable(); + component.filterFamilyDiseaseList(m[3], 0, row); + expect(list().length).toBe(1); + expect(component.diseaseSelectList[0]).toContain(m[0]); + expect(row.get('familyMembers')?.disabled).toBeTrue(); + component.filterFamilyDiseaseList(m[2], 0); + expect(component.previousSelectedDiseaseList[0]).toBe(m[2]); + }); + + it('removeFamilyDisease cancelled does nothing', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeFamilyDisease(0, list().at(0)); + expect(list().length).toBe(1); + expect(component.familyHistoryForm.dirty).toBeFalse(); + }); + + it('removeFamilyDisease single unsaved row resets it and clears DM score', () => { + const row = list().at(0); + row.patchValue({ diseaseType: m[1] }); + component.previousSelectedDiseaseList = [m[1]]; + component.removeFamilyDisease(0, row); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(row.value.diseaseType).toBeNull(); + expect(row.value.deleted).toBeFalse(); + expect(component.diabetesPresent).toBeFalse(); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + }); + + it('removeFamilyDisease single unsaved non-DM row does not reset score', () => { + component.previousSelectedDiseaseList = [m[0]]; + component.removeFamilyDisease(0, list().at(0)); + expect(idrs.setIDRSFamilyScore).not.toHaveBeenCalled(); + expect(list().length).toBe(1); + }); + + it('removeFamilyDisease single saved row adds a fresh row', () => { + list().at(0).patchValue({ ID: 9, diseaseType: m[1] }); + component.previousSelectedDiseaseList = [m[1]]; + component.removeFamilyDisease(0, list().at(0)); + expect(list().length).toBe(2); + expect(component.diseaseSelectList[1]).toBe(component.diseaseMasterData); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + idrs.setIDRSFamilyScore.calls.reset(); + // non-DM saved single row + component.familyHistoryForm.setControl( + 'familyDiseaseList', + new FormArray([component.initFamilyDiseaseList()]) + ); + list().at(0).patchValue({ ID: 4 }); + component.previousSelectedDiseaseList = [m[0]]; + component.removeFamilyDisease(0, list().at(0)); + expect(list().length).toBe(2); + expect(idrs.setIDRSFamilyScore).not.toHaveBeenCalled(); + }); + + it('removeFamilyDisease unsaved row among many removes it and returns value', () => { + component.addFamilyDisease(); + component.previousSelectedDiseaseList = [m[0], m[1]]; + component.diseaseSelectList = [[m[0]], [m[1]]]; + component.removeFamilyDisease(1, list().at(1)); + expect(list().length).toBe(1); + expect(component.diseaseSelectList).toEqual([[m[0], m[1]]]); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + }); + + it('removeFamilyDisease Other row among many does not return it', () => { + component.addFamilyDisease(); + component.previousSelectedDiseaseList = [m[0], m[4]]; + component.diseaseSelectList = [[m[0]], [m[4]]]; + component.removeFamilyDisease(1); + expect(component.diseaseSelectList).toEqual([[m[0]]]); + expect(idrs.setIDRSFamilyScore).not.toHaveBeenCalled(); + }); + + it('removeFamilyDisease saved rows are flagged deleted; last one adds a fresh row', () => { + component.addFamilyDisease(); + list().at(0).patchValue({ ID: 3 }); + list().at(1).patchValue({ ID: 7 }); + component.previousSelectedDiseaseList = [m[0], m[1]]; + component.removeFamilyDisease(1, list().at(1)); + expect(list().length).toBe(2); + expect(component.diseaseSelectList[1]).toEqual([]); + component.removeFamilyDisease(0, list().at(0)); + expect(list().length).toBe(3); + expect(component.diseaseSelectList[2]).toBe(component.diseaseMasterData); + }); + + it('checkValidity requires disease and family members', () => { + const g: any = component.initFamilyDiseaseList(); + expect(component.checkValidity(g)).toBeTrue(); + g.get('familyMembers').enable(); + g.patchValue({ diseaseType: m[0], familyMembers: ['Father'] }); + expect(component.checkValidity(g)).toBeFalse(); + }); + }); + + describe('misc', () => { + beforeEach(() => fixture.detectChanges()); + + it('isGeneticDisorder getter and resetOtherGeneticOrder', () => { + component.familyHistoryForm.patchValue({ + isGeneticDisorder: true, + geneticDisorder: 'x', + }); + expect(component.isGeneticDisorder).toBeTrue(); + component.resetOtherGeneticOrder(); + expect(component.familyHistoryForm.value.geneticDisorder).toBeNull(); + }); + + it('sortDiseaseList sorts by diseaseType', () => { + const arr = [ + { diseaseType: 'b' }, + { diseaseType: 'a' }, + { diseaseType: 'a' }, + ]; + component.sortDiseaseList(arr); + expect(arr.map(a => a.diseaseType)).toEqual(['a', 'a', 'b']); + }); + + const dmGroup = { + value: { diseaseType: { diseaseType: 'Diabetes Mellitus' } }, + }; + + it('filterFamilyMembers scores single and both parents for diabetes', () => { + component.filterFamilyMembers({ value: ['Father', 'Brother'] }, dmGroup); + expect(session.setItem).toHaveBeenCalledWith( + 'IdRSScoreFamilyHistory', + '10' + ); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(10); + expect(idrs.setIDRSScoreFlag).toHaveBeenCalled(); + component.filterFamilyMembers({ value: ['Mother', 'Father'] }, dmGroup); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(20); + component.filterFamilyMembers({ value: ['Father', 'Mother'] }, dmGroup); + expect(session.setItem).toHaveBeenCalledWith( + 'IdRSScoreFamilyHistory', + '20' + ); + component.filterFamilyMembers({ value: [] }, dmGroup); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + expect(component.familyMembersArray).toEqual([]); + }); + + it('filterFamilyMembers ignores non-diabetes diseases', () => { + component.filterFamilyMembers( + { value: ['Father'] }, + { value: { diseaseType: { diseaseType: 'Asthma' } } } + ); + expect(idrs.setIDRSFamilyScore).not.toHaveBeenCalled(); + expect(component.familyMembersArray).toEqual(['Father']); + }); + + it('patchFamilyMembersIDRSScore computes score', () => { + component.patchFamilyMembersIDRSScore(['Mother']); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(10); + component.patchFamilyMembersIDRSScore(['Mother', 'Father']); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(20); + component.patchFamilyMembersIDRSScore(['Father', 'Mother']); + component.patchFamilyMembersIDRSScore([]); + expect(idrs.setIDRSFamilyScore).toHaveBeenCalledWith(0); + }); + + it('getPreviousFamilyHistory opens dialog when data exists', () => { + component.visitCategory = 'NCD screening'; + const data = { data: [{}] }; + nurse.getPreviousFamilyHistory.and.returnValue( + of({ statusCode: 200, data }) + ); + component.getPreviousFamilyHistory(); + expect(nurse.getPreviousFamilyHistory).toHaveBeenCalledWith( + 'b1', + 'NCD screening' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: data, + title: LANGUAGE_EN.historyData.familyhistory.previousfamilyhistory, + }, + }); + }); + + it('getPreviousFamilyHistory alerts on empty, failure and error', () => { + nurse.getPreviousFamilyHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + nurse.getPreviousFamilyHistory.and.returnValue(of({ statusCode: 500 })); + component.getPreviousFamilyHistory(); + nurse.getPreviousFamilyHistory.and.returnValue(throwingObs()); + component.getPreviousFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + expect(confirm.alert).toHaveBeenCalledTimes(3); + }); + + it('ngOnDestroy unsubscribes and tolerates missing subscriptions', () => { + component.getGeneralHistory('b1', 'v1'); + const subs = [ + component.nurseMasterDataSubscription, + component.generalHistorySubscription, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history/family-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history/family-history.component.spec.ts index 21988c84..595ed50d 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history/family-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/family-history/family-history.component.spec.ts @@ -20,76 +20,353 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { DoctorService, - NurseService, MasterdataService, + NurseService, } from '../../../shared/services'; - -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; -import { NurseServiceStub } from '../../../shared/mocks/nurse-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { FamilyHistoryComponent } from './family-history.component'; -describe('FamilyHistoryComponent', () => { +describe('General FamilyHistoryComponent', () => { let component: FamilyHistoryComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let form: FormGroup; + let D: any, H: any, NONE: any, OTHER: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - schemas: [NO_ERRORS_SCHEMA], + function masterData() { + D = { diseaseType: 'Diabetes', snomedCode: 'd1', snomedTerm: 'dt' }; + H = { diseaseType: 'Hypertension', snomedCode: 'h1', snomedTerm: 'ht' }; + NONE = { diseaseType: 'None' }; + OTHER = { diseaseType: 'Other' }; + return { + DiseaseTypes: [D, H, NONE, OTHER], + familyMemberTypes: ['Mother', 'Father'], + }; + } + + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [FamilyHistoryComponent], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, - { provide: NurseService, useClass: NurseServiceStub }, + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(FamilyHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(FamilyHistoryComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + form = new FormGroup({ + familyDiseaseList: new FormArray([]), + geneticDisorder: new FormControl(null), + isConsanguineousMarrige: new FormControl(null), + isGeneticDisorder: new FormControl(null), + }); + component.familyHistoryForm = form; + component.mode = mode; + component.visitCategory = 'General OPD'; + fixture.detectChanges(); + } - fb = debugElement.injector.get(FormBuilder); - component.familyHistoryForm = new GeneralUtils( - fb - ).createFamilyHistoryForm(); - window.console.log = () => {}; + const list = () => form.controls['familyDiseaseList'] as FormArray; + const names = (arr: any[]) => arr.map(a => a.diseaseType); - fixture.detectChanges(); + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('sets language and waits for master data', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(list().length).toBe(0); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ngOnDestroy unsubscribes', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.generalHistorySubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('getFamilyDiseases returns controls or null', () => { + expect(component.getFamilyDiseases()).toEqual([]); + component.familyHistoryForm = new FormGroup({ + familyDiseaseList: new FormControl(null), + }); + expect(component.getFamilyDiseases()).toBeNull(); + }); + + it('addFamilyDisease without master list only pushes a row', () => { + component.diseaseMasterData = null; + component.addFamilyDisease(); + expect(list().length).toBe(1); + expect(component.diseaseSelectList.length).toBe(0); + expect(list().at(0).get('familyMembers')?.disabled).toBeTrue(); + }); + + describe('with master data', () => { + beforeEach(() => master$.next(masterData())); + + it('stores master data and adds first row with all diseases', () => { + expect(component.diseaseMasterData.length).toBe(4); + expect(component.familyMemeberMasterData).toEqual(['Mother', 'Father']); + expect(list().length).toBe(1); + expect(component.diseaseSelectList[0].length).toBe(4); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('next row excludes None and already-selected, keeps Other', () => { + list().at(0).patchValue({ diseaseType: D }); + component.addFamilyDisease(); + expect(names(component.diseaseSelectList[1])).toEqual([ + 'Hypertension', + 'Other', + ]); + list().at(1).patchValue({ diseaseType: OTHER }); + component.addFamilyDisease(); + expect(names(component.diseaseSelectList[2])).toEqual([ + 'Hypertension', + 'Other', + ]); + }); + + it('filterFamilyDiseaseList patches snomed, removes from other lists and enables members', () => { + component.addFamilyDisease(); + const row = list().at(0); + component.filterFamilyDiseaseList(D, 0, row); + expect(row.value.snomedCode).toBe('d1'); + expect(row.value.snomedTerm).toBe('dt'); + expect(row.get('familyMembers')?.enabled).toBeTrue(); + expect(component.diseaseSelectList[1]).not.toContain(D); + expect(component.previousSelectedDiseaseList[0]).toBe(D); + }); + + it('filterFamilyDiseaseList returns previous value to other lists', () => { + component.addFamilyDisease(); + const row = list().at(0); + component.filterFamilyDiseaseList(D, 0, row); + component.filterFamilyDiseaseList(H, 0, row); + expect(component.diseaseSelectList[1]).toContain(D); + expect(component.diseaseSelectList[1]).not.toContain(H); + }); + + it('filterFamilyDiseaseList with Other keeps other lists and snomed', () => { + component.addFamilyDisease(); + const row = list().at(0); + row.patchValue({ snomedCode: 'keep' }); + component.filterFamilyDiseaseList(OTHER, 0, row); + expect(row.value.snomedCode).toBe('keep'); + expect(component.diseaseSelectList[1]).toContain(OTHER); + component.filterFamilyDiseaseList(D, 0, row); + expect( + component.diseaseSelectList[1].filter((x: any) => x === OTHER).length + ).toBe(1); + }); + + it('selecting None removes other rows and disables members', () => { + component.addFamilyDisease(); + component.filterFamilyDiseaseList(H, 1, list().at(1)); + component.addFamilyDisease(); + const row = list().at(0); + component.filterFamilyDiseaseList(NONE, 0, row); + expect(list().length).toBe(1); + expect(component.diseaseSelectList.length).toBe(1); + expect(component.diseaseSelectList[0]).toContain(H); + expect(row.get('familyMembers')?.disabled).toBeTrue(); + }); + + it('filterFamilyDiseaseList works without a form', () => { + component.filterFamilyDiseaseList(D, 0); + expect(component.previousSelectedDiseaseList[0]).toBe(D); + }); + + describe('removeFamilyDisease', () => { + it('does nothing when cancelled', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeFamilyDisease(0, list().at(0)); + expect(list().length).toBe(1); + expect(form.dirty).toBeFalse(); + }); + + it('resets the only row', () => { + const row = list().at(0); + row.patchValue({ diseaseType: D }); + component.removeFamilyDisease(0, row); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(row.value.diseaseType).toBeNull(); + expect(list().length).toBe(1); + }); + + it('removes a row and returns its disease to other lists', () => { + component.addFamilyDisease(); + component.filterFamilyDiseaseList(H, 1, list().at(1)); + expect(component.diseaseSelectList[0]).not.toContain(H); + component.removeFamilyDisease(1, list().at(1)); + expect(list().length).toBe(1); + expect(component.diseaseSelectList.length).toBe(1); + expect(component.diseaseSelectList[0]).toContain(H); + expect(form.dirty).toBeTrue(); + }); + + it('removes a row without form or selection', () => { + component.addFamilyDisease(); + component.removeFamilyDisease(1); + expect(list().length).toBe(1); + }); + }); + }); + + describe('getPreviousFamilyHistory', () => { + const info = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousFamilyHistory.and.returnValue( + of({ statusCode: 200, data: payload }) + ); + component.getPreviousFamilyHistory(); + expect(nurse.getPreviousFamilyHistory).toHaveBeenCalledWith( + '11', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: LANGUAGE_EN.historyData.familyhistory.previousfamilyhistory, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousFamilyHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + }); + it('alerts error on non-200', () => { + nurse.getPreviousFamilyHistory.and.returnValue(of({ statusCode: 500 })); + component.getPreviousFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith(info(), 'error'); + }); + it('alerts error on failure', () => { + nurse.getPreviousFamilyHistory.and.returnValue(throwingObs()); + component.getPreviousFamilyHistory(); + expect(confirm.alert).toHaveBeenCalledWith(info(), 'error'); + }); + }); + + it('isGeneticDisorder getter and reset', () => { + form.patchValue({ isGeneticDisorder: 'Yes', geneticDisorder: 'x' }); + expect(component.isGeneticDisorder).toBe('Yes'); + component.resetOtherGeneticOrder(); + expect(form.value.geneticDisorder).toBeNull(); + }); + + it('sortDiseaseList sorts including equal items', () => { + const l = [ + { diseaseType: 'b' }, + { diseaseType: 'a' }, + { diseaseType: 'b' }, + ]; + component.sortDiseaseList(l); + expect(names(l)).toEqual(['a', 'b', 'b']); + }); + + it('checkValidity', () => { + expect(component.checkValidity({ value: {} })).toBeTrue(); + expect( + component.checkValidity({ + value: { diseaseType: 'a', familyMembers: ['m'] }, + }) + ).toBeFalse(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('fetches and patches family history', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + FamilyHistory: { + familyDiseaseList: [ + { diseaseType: 'Diabetes', familyMembers: ['Mother'] }, + { diseaseType: 'Unknown', otherDiseaseType: 'z' }, + { diseaseType: null }, + ], + geneticDisorder: 'g', + isConsanguineousMarrige: 'No', + isGeneticDisorder: 'Yes', + }, + }, + }) + ); + master$.next(masterData()); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.geneticDisorder).toBe('g'); + expect(form.value.isGeneticDisorder).toBe('Yes'); + expect(list().length).toBe(3); + const first = list().at(0); + expect(first.value.diseaseType).toBe(D); + expect(first.get('familyMembers')?.enabled).toBeTrue(); + expect(first.dirty).toBeTrue(); + expect(list().at(1).value.diseaseType).toBe('Unknown'); + expect(list().at(2).value.diseaseType).toBeNull(); + }); + + it('ignores response without FamilyHistory', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + master$.next(masterData()); + expect(component.familyHistoryData).toBeUndefined(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/feeding-history/feeding-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/feeding-history/feeding-history.component.spec.ts index 6f10ccb8..46953570 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/feeding-history/feeding-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/feeding-history/feeding-history.component.spec.ts @@ -20,82 +20,239 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { DoctorService, - NurseService, MasterdataService, + NurseService, } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; -import { NurseServiceStub } from '../../../shared/mocks/nurse-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { FeedingHistoryComponent } from './feeding-history.component'; +const CONTROLS = [ + 'typeOfFeed', + 'compFeedStartAge', + 'noOfCompFeedPerDay', + 'foodIntoleranceStatus', + 'typeofFoodIntolerance', +]; + describe('FeedingHistoryComponent', () => { let component: FeedingHistoryComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - schemas: [NO_ERRORS_SCHEMA], + async function setup(mode = 'new', ben: any = null) { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(ben); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [FeedingHistoryComponent], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, - { provide: NurseService, useClass: NurseServiceStub }, + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: ben$.asObservable() }, }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(FeedingHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(FeedingHistoryComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + const g: any = {}; + CONTROLS.forEach(c => (g[c] = new FormControl(null))); + form = new FormGroup(g); + component.feedingHistoryForm = form; + component.mode = mode; + component.visitCategory = 'General OPD'; + fixture.detectChanges(); + } - fb = debugElement.injector.get(FormBuilder); - component.feedingHistoryForm = new GeneralUtils( - fb - ).createFeedingHistoryForm(); - window.console.log = () => {}; + describe('new mode', () => { + beforeEach(async () => setup('new')); - fixture.detectChanges(); + it('sets language, age stays 0 without beneficiary, master data stored', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.age).toBe(0); + const m = { a: 1 }; + master$.next(m); + expect(component.masterData).toBe(m); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('computes age in months from "years - months" string', () => { + ben$.next({ age: '2 years - 3 months' }); + expect(component.age).toBe(27); + }); + + it('computes age in months from "months" string', () => { + ben$.next({ age: '7 months - 4 days' }); + expect(component.age).toBe(7); + }); + + it('leaves age unchanged when neither years nor months', () => { + ben$.next({ age: '10 days' }); + expect(component.age).toBe(0); + }); + + it('ngOnDestroy unsubscribes when present and tolerates nulls', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.beneficiaryDetailSubscription, 'unsubscribe'); + const c = spyOn(component.generalHistorySubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + expect(c).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.beneficiaryDetailSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + describe('getGeneralHistory', () => { + it('patches form with FeedingHistory', () => { + const fh = { typeOfFeed: 'Breast', compFeedStartAge: 6 }; + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { FeedingHistory: fh } }) + ); + component.getGeneralHistory('1', '2'); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('1', '2'); + expect(component.feedingHistoryData).toBe(fh); + expect(form.value.typeOfFeed).toBe('Breast'); + expect(component.compFeedStartAge).toBe(6); + }); + + [ + null, + { statusCode: 500, data: { FeedingHistory: {} } }, + { statusCode: 200, data: null }, + { statusCode: 200, data: {} }, + ].forEach((resp, i) => { + it(`ignores unusable response #${i}`, () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(resp)); + component.getGeneralHistory('1', '2'); + expect(component.feedingHistoryData).toBeUndefined(); + expect(form.value.typeOfFeed).toBeNull(); + }); + }); + }); + + it('getters and reset helpers', () => { + form.patchValue({ + foodIntoleranceStatus: 'Yes', + noOfCompFeedPerDay: 3, + typeofFoodIntolerance: 'Milk', + }); + expect(component.foodIntoleranceStatus).toBe('Yes'); + component.resetNoOfCompFeedPerDay(); + expect(form.value.noOfCompFeedPerDay).toBeNull(); + component.resetTypeofFoodIntolerance(); + expect(form.value.typeofFoodIntolerance).toBeNull(); + }); + + describe('getPreviousFeedingHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousFeedingHistory.and.returnValue(of({ data: payload })); + component.getPreviousFeedingHistory(); + expect(nurse.getPreviousFeedingHistory).toHaveBeenCalledWith( + '11', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.Perinatalhistorydetails + .developmentalhistorydetails, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousFeedingHistory.and.returnValue( + of({ data: { data: [] } }) + ); + component.getPreviousFeedingHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + expect(dialog.open).not.toHaveBeenCalled(); + }); + it('alerts when data null', () => { + nurse.getPreviousFeedingHistory.and.returnValue(of({ data: null })); + component.getPreviousFeedingHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts when response null', () => { + nurse.getPreviousFeedingHistory.and.returnValue(of(null)); + component.getPreviousFeedingHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousFeedingHistory.and.returnValue(throwingObs()); + component.getPreviousFeedingHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('initial beneficiary', () => { + beforeEach(async () => setup('new', { age: '1 years - 1 months' })); + it('computes age on init', () => { + expect(component.age).toBe(13); + }); + }); + + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('loads history with session IDs when master data arrives', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { FeedingHistory: { typeOfFeed: 'F' } } }) + ); + master$.next({ a: 1 }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.typeOfFeed).toBe('F'); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/general-opd-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/general-opd-history.component.spec.ts index aa40d705..640d08a5 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/general-opd-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/general-opd-history.component.spec.ts @@ -20,55 +20,242 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { GeneralUtils } from '../../shared/utility'; - -import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../shared/services'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { DoctorService } from '../../shared/services'; import { GeneralOpdHistoryComponent } from './general-opd-history.component'; describe('GeneralOpdHistoryComponent', () => { let component: GeneralOpdHistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let benSvc: any; + let confirm: any; + let session: any; + let ben$: BehaviorSubject; + + const sections = [ + 'pastHistory', + 'comorbidityHistory', + 'medicationHistory', + 'personalHistory', + 'familyHistory', + 'menstrualHistory', + 'perinatalHistory', + 'pastObstericHistory', + 'immunizationHistory', + 'otherVaccines', + 'feedingHistory', + 'developmentHistory', + 'physicalActivityHistory', + ]; + const makeForm = () => { + const g: any = {}; + sections.forEach(s => (g[s] = new FormGroup({}))); + return new FormGroup(g); + }; + const expectedTemp = { + beneficiaryRegID: 'b1', + benVisitID: 'v1', + providerServiceMapID: 'p1', + createdBy: 'nurse1', + modifiedBy: 'nurse1', + beneficiaryID: 'bid', + sessionID: 's1', + parkingPlaceID: 8, + vanID: 7, + benFlowID: 'f1', + visitCode: 'vc', + }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + doctor = autoSpy(DoctorService); + ben$ = new BehaviorSubject(null); + benSvc = autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralOpdHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 7, parkingPlaceID: 8 }), + beneficiaryRegID: 'b1', + visitID: 'v1', + providerServiceID: 'p1', + userName: 'nurse1', + beneficiaryID: 'bid', + sessionID: 's1', + benFlowID: 'f1', + visitCode: 'vc', + visitCategory: 'General OPD', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: BeneficiaryDetailsService, useValue: benSvc }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(GeneralOpdHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + component.nurseGeneralHistoryForm = makeForm(); + component.ngOnInit(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('ngOnInit wires sub forms and language', () => { + sections.forEach(s => + expect((component as any)[s]).toBe( + component.nurseGeneralHistoryForm.get(s) + ) + ); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('beneficiary details decide obstetric history visibility', () => { + ben$.next({ genderName: 'Female', ageVal: 25 }); + expect(component.beneficiary.ageVal).toBe(25); + expect(component.showObstetricHistory).toBeTrue(); + ben$.next({ genderName: 'Male', ageVal: 25 }); + expect(component.showObstetricHistory).toBeFalse(); + ben$.next({ genderName: 'Female', ageVal: 10 }); + expect(component.showObstetricHistory).toBeFalse(); + }); + + it('canShowObstetricHistory handles primigravida and missing beneficiary', () => { + component.primiGravida = true; + component.showObstetricHistory = true; + component.canShowObstetricHistory(); + expect(component.showObstetricHistory).toBeFalse(); + component.primiGravida = false; + component.visitCategory = 'PNC'; + component.canShowObstetricHistory(); + expect(component.showObstetricHistory).toBeTrue(); + component.visitCategory = 'ANC'; + component.showObstetricHistory = false; + component.canShowObstetricHistory(); + expect(component.showObstetricHistory).toBeTrue(); + }); + + it('ngOnChanges re-evaluates obstetric history on pregnancy / primigravida change', () => { + spyOn(component, 'canShowObstetricHistory'); + component.ngOnChanges({ pregnancyStatus: {} }); + component.ngOnChanges({ primiGravida: {} }); + component.ngOnChanges({ mode: {} }); + expect(component.canShowObstetricHistory).toHaveBeenCalledTimes(2); + expect(doctor.updateGeneralHistory).not.toHaveBeenCalled(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('update mode', () => { + beforeEach(() => { + ben$.next({ genderName: 'Female', ageVal: 30 }); + component.mode = 'update'; + }); + + it('updates general history and refreshes HRP for ANC (positive)', () => { + component.visitCategory = 'ANC'; + component.nurseGeneralHistoryForm.markAsDirty(); + doctor.updateGeneralHistory.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) + ); + component.ngOnChanges({ mode: {} }); + expect(doctor.updateGeneralHistory).toHaveBeenCalledWith( + component.nurseGeneralHistoryForm, + expectedTemp, + 30 + ); + expect(doctor.getHRPDetails).toHaveBeenCalledWith('b1', 'vc'); + expect(benSvc.setHRPPositive).toHaveBeenCalled(); + expect(confirm.alert).toHaveBeenCalledWith('saved', 'success'); + expect(component.nurseGeneralHistoryForm.pristine).toBeTrue(); + }); + + it('resets HRP when not HRP and ignores empty HRP response', () => { + component.visitCategory = 'ANC'; + doctor.updateGeneralHistory.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: false } }) + ); + component.updatePatientGeneralHistory(component.nurseGeneralHistoryForm); + expect(benSvc.resetHRPPositive).toHaveBeenCalled(); + doctor.getHRPDetails.and.returnValue(of(null)); + component.getHRPDetails(); + expect(benSvc.resetHRPPositive).toHaveBeenCalledTimes(1); + expect(benSvc.setHRPPositive).not.toHaveBeenCalled(); + }); + + it('does not fetch HRP for non-ANC; alerts errors', () => { + component.visitCategory = 'General OPD'; + doctor.updateGeneralHistory.and.returnValue( + of({ statusCode: 200, data: { response: 'ok' } }) + ); + component.updatePatientGeneralHistory(component.nurseGeneralHistoryForm); + expect(doctor.getHRPDetails).not.toHaveBeenCalled(); + doctor.updateGeneralHistory.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.updatePatientGeneralHistory(component.nurseGeneralHistoryForm); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + doctor.updateGeneralHistory.and.returnValue(throwingObs('boom')); + component.updatePatientGeneralHistory(component.nurseGeneralHistoryForm); + expect(confirm.alert).toHaveBeenCalledWith('boom', 'error'); + }); + + it('uses NCD screening history update for NCD screening visits', () => { + session.store.set('visitCategory', 'NCD screening'); + component.nurseGeneralHistoryForm.markAsDirty(); + doctor.updateNCDScreeningHistory.and.returnValue( + of({ statusCode: 200, data: { response: 'ncd saved' } }) + ); + component.ngOnChanges({ mode: {} }); + expect(doctor.updateGeneralHistory).not.toHaveBeenCalled(); + expect(doctor.updateNCDScreeningHistory).toHaveBeenCalledWith( + component.nurseGeneralHistoryForm, + expectedTemp, + 30 + ); + expect(confirm.alert).toHaveBeenCalledWith('ncd saved', 'success'); + expect(component.nurseGeneralHistoryForm.pristine).toBeTrue(); + }); + + it('NCD screening history update alerts errors', () => { + doctor.updateNCDScreeningHistory.and.returnValue( + of({ statusCode: 500, errorMessage: 'no' }) + ); + component.updatePatientNCDScreeningHistory( + component.nurseGeneralHistoryForm + ); + doctor.updateNCDScreeningHistory.and.returnValue(throwingObs('err')); + component.updatePatientNCDScreeningHistory( + component.nurseGeneralHistoryForm + ); + expect(confirm.alert.calls.allArgs()).toEqual([ + ['no', 'error'], + ['err', 'error'], + ]); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/immunization-history/immunization-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/immunization-history/immunization-history.component.spec.ts index 8c1e4b9c..40fe2444 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/immunization-history/immunization-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/immunization-history/immunization-history.component.spec.ts @@ -20,76 +20,226 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { ImmunizationHistoryComponent } from './immunization-history.component'; describe('ImmunizationHistoryComponent', () => { let component: ImmunizationHistoryComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let doctor: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - schemas: [NO_ERRORS_SCHEMA], + const vaccinations = [ + { + vaccinationTime: '6 Weeks', + vaccineName: 'OPV1', + sctCode: 's1', + sctTerm: 't1', + }, + { + vaccinationTime: 'At Birth', + vaccineName: 'BCG', + sctCode: null, + sctTerm: null, + }, + { + vaccinationTime: '6 Weeks', + vaccineName: 'DPT1', + sctCode: 's2', + sctTerm: 't2', + }, + { + vaccinationTime: '9 Months', + vaccineName: 'MR', + sctCode: 's3', + sctTerm: 't3', + }, + { + vaccinationTime: '5 Years', + vaccineName: 'DT', + sctCode: 's4', + sctTerm: 't4', + }, + ]; + + async function setup(mode = 'new', age: any = '1 years - 2 months') { + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(age === 'NULL_BEN' ? null : { age }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ImmunizationHistoryComponent], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: ben$.asObservable() }, }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(ImmunizationHistoryComponent, '') + .compileComponents(); + spyOn(console, 'log'); fixture = TestBed.createComponent(ImmunizationHistoryComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + form = new FormGroup({ immunizationList: new FormArray([]) }); + component.immunizationHistoryForm = form; + component.mode = mode; + fixture.detectChanges(); + } - fb = debugElement.injector.get(FormBuilder); - component.immunizationHistoryForm = new GeneralUtils( - fb - ).createImmunizationHistoryForm(); - window.console.log = () => {}; + const list = () => form.controls['immunizationList'] as FormArray; - fixture.detectChanges(); + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('sets language and ignores master data without vaccinations', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + master$.next({ other: 1 }); + expect(component.masterData).toBeUndefined(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('builds age-filtered, sorted immunization groups', () => { + master$.next({ childVaccinations: vaccinations }); + expect(component.beneficiaryAge).toBe('1 years'); + expect(component.temp.map((t: any) => t.defaultReceivingAge)).toEqual([ + 'At Birth', + '6 Weeks', + '9 Months', + ]); + expect(component.temp[1].vaccines).toEqual([ + { vaccine: 'OPV1', sctCode: 's1', sctTerm: 't1', status: false }, + { vaccine: 'DPT1', sctCode: 's2', sctTerm: 't2', status: false }, + ]); + expect(component.temp[0].vaccines[0]).toEqual({ + vaccine: 'BCG', + sctCode: null, + sctTerm: null, + status: false, + }); + expect(list().length).toBe(3); + expect((list().at(1).get('vaccines') as FormArray).length).toBe(2); + expect(list().at(1).value.defaultReceivingAge).toBe('6 Weeks'); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('selectAll toggles every vaccine status', () => { + master$.next({ childVaccinations: vaccinations }); + component.selectAll({ checked: true }, 1); + const vs = () => (list().at(1).get('vaccines') as FormArray).value; + expect(vs().every((v: any) => v.status === true)).toBeTrue(); + expect(list().dirty).toBeTrue(); + component.selectAll({ checked: false }, 1); + expect(vs().every((v: any) => v.status === false)).toBeTrue(); + }); + + it('getAgeValue converts units', () => { + expect(component.getAgeValue(null)).toBe(0); + expect(component.getAgeValue('2 years')).toBe(720); + expect(component.getAgeValue('3 Months')).toBe(90); + expect(component.getAgeValue('2 weeks')).toBe(14); + expect(component.getAgeValue('4 days')).toBe(0); + expect(component.getAgeValue('Birth')).toBe(0); + }); + + it('ngOnDestroy unsubscribes', () => { + master$.next({ childVaccinations: [] }); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.beneficiaryDetailSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('beneficiary without age', () => { + beforeEach(async () => setup('new', null)); + + it('keeps only zero-age vaccinations', () => { + master$.next({ childVaccinations: vaccinations }); + expect(component.beneficiaryAge).toBeUndefined(); + expect(component.temp.map((t: any) => t.defaultReceivingAge)).toEqual([ + 'At Birth', + ]); + }); + }); + + describe('null beneficiary', () => { + beforeEach(async () => setup('new', 'NULL_BEN')); + + it('does not set age', () => { + master$.next({ childVaccinations: vaccinations }); + expect(component.beneficiaryAge).toBeUndefined(); + }); + }); + + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('loads history and computes select-all flags', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + ImmunizationHistory: { + immunizationList: [ + { + defaultReceivingAge: 'At Birth', + vaccines: [{ vaccine: 'BCG', status: true }], + }, + { + defaultReceivingAge: '6 Weeks', + vaccines: [ + { vaccine: 'OPV1', status: true }, + { vaccine: 'DPT1', status: false }, + ], + }, + ], + }, + }, + }) + ); + master$.next({ childVaccinations: vaccinations }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(component.checkSelectALL).toEqual([true, false]); + expect( + (list().at(0).get('vaccines') as FormArray).at(0).value.status + ).toBeTrue(); + }); + + it('ignores responses without immunization list', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { ImmunizationHistory: {} } }) + ); + master$.next({ childVaccinations: vaccinations }); + expect(component.checkSelectALL).toEqual([]); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/medication-history/medication-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/medication-history/medication-history.component.spec.ts index 8547f74d..7279a475 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/medication-history/medication-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/medication-history/medication-history.component.spec.ts @@ -20,55 +20,324 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { MedicationHistoryComponent } from './medication-history.component'; describe('MedicationHistoryComponent', () => { let component: MedicationHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject({ age: '2 years' }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [MedicationHistoryComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(MedicationHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(MedicationHistoryComponent); component = fixture.componentInstance; + form = new FormGroup({ medicationHistoryList: new FormArray([]) }); + component.medicationHistoryForm = form; + component.mode = mode; + component.visitCategory = 'General OPD'; fixture.detectChanges(); + } + + const list = () => + component.medicationHistoryForm.controls[ + 'medicationHistoryList' + ] as FormArray; + + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('initialises language, beneficiary and first row', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.beneficiary).toEqual({ age: '2 years' }); + expect(list().length).toBe(1); + expect(list().at(0).get('timePeriodAgo')?.disabled).toBeTrue(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('stores master data without fetching history', () => { + master$.next({ a: 1 }); + expect(component.masterData).toEqual({ a: 1 }); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('ngOnDestroy unsubscribes', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const spies = [ + component.nurseMasterDataSubscription, + component.generalHistorySubscription, + component.beneficiaryDetailSubscription, + ].map(s => spyOn(s, 'unsubscribe')); + component.ngOnDestroy(); + spies.forEach(s => expect(s).toHaveBeenCalled()); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('getMedicationHistory returns controls or null', () => { + expect(component.getMedicationHistory()?.length).toBe(1); + component.medicationHistoryForm = new FormGroup({ + medicationHistoryList: new FormControl(null), + }); + expect(component.getMedicationHistory()).toBeNull(); + }); + + it('addMedicationHistory is safe without a form', () => { + component.medicationHistoryForm = undefined as any; + expect(() => component.addMedicationHistory()).not.toThrow(); + }); + + it('createMedicationHistoryForm builds a one-row form', () => { + component.createMedicationHistoryForm(); + expect(list().length).toBe(1); + expect(component.medicationHistoryForm).not.toBe(form); + }); + + describe('removeMedicationHistory', () => { + it('does nothing when cancelled', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeMedicationHistory(0, list().at(0)); + expect(form.dirty).toBeFalse(); + }); + + it('clears the only row', () => { + spyOn(console, 'log'); + list().at(0).patchValue({ currentMedication: 'x' }); + component.removeMedicationHistory(0, list().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(list().length).toBe(1); + expect(list().at(0).get('currentMedication')?.value).toBeNull(); + expect(form.dirty).toBeTrue(); + }); + + it('removes a row and marks parent dirty', () => { + spyOn(console, 'log'); + const parent = new FormGroup({ med: form }); + component.addMedicationHistory(); + component.removeMedicationHistory(1, list().at(1)); + expect(list().length).toBe(1); + expect(parent.dirty).toBeTrue(); + }); + + it('removes the only row when no form passed', () => { + spyOn(console, 'log'); + component.removeMedicationHistory(0); + expect(list().length).toBe(0); + }); + }); + + describe('getPreviousMedicationHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousMedicationHistory.and.returnValue( + of({ statusCode: 200, data: payload }) + ); + component.getPreviousMedicationHistory(); + expect(nurse.getPreviousMedicationHistory).toHaveBeenCalledWith( + '11', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.Medicationhistorydetails + .previousmedicationhistorydetails, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousMedicationHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousMedicationHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + }); + it('alerts on non-200', () => { + nurse.getPreviousMedicationHistory.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getPreviousMedicationHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousMedicationHistory.and.returnValue(throwingObs()); + component.getPreviousMedicationHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); + + describe('validateDuration', () => { + let row: any; + beforeEach(() => { + row = list().at(0); + row.get('timePeriodAgo').enable(); + }); + + it('enables unit when only duration given', () => { + row.patchValue({ timePeriodAgo: 3 }); + component.validateDuration(row); + expect(row.get('timePeriodUnit').enabled).toBeTrue(); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + + it('disables unit when duration empty', () => { + row.get('timePeriodUnit').enable(); + component.validateDuration(row); + expect(row.get('timePeriodUnit').disabled).toBeTrue(); + }); + + it('accepts a duration within age', () => { + row.get('timePeriodUnit').enable(); + row.patchValue({ timePeriodAgo: 1, timePeriodUnit: 'Years' }); + component.validateDuration(row); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(row.value.timePeriodAgo).toBe(1); + expect(row.value.timePeriodUnit).toBe('Years'); + }); + + it('rejects a duration greater than age', () => { + row.get('timePeriodUnit').enable(); + row.patchValue({ timePeriodAgo: 5, timePeriodUnit: 'Years' }); + component.validateDuration(row); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.durationGreaterThanAge + ); + expect(row.value.timePeriodAgo).toBeNull(); + expect(row.value.timePeriodUnit).toBeNull(); + // unit toggling uses the pre-reset local values, so it stays enabled + expect(row.get('timePeriodUnit').enabled).toBeTrue(); + }); + }); + + it('checkValidity', () => { + const row = list().at(0); + expect(component.checkValidity(row)).toBeTrue(); + row.patchValue({ + currentMedication: 'm', + timePeriodAgo: 1, + timePeriodUnit: 'Days', + }); + expect(component.checkValidity(row)).toBeFalse(); + }); + + it('enableDuration toggles duration controls', () => { + const row = list().at(0); + row.patchValue({ currentMedication: 'm' }); + component.enableDuration(row); + expect(row.get('timePeriodAgo')?.enabled).toBeTrue(); + row.patchValue({ currentMedication: null }); + row.get('timePeriodUnit')?.enable(); + component.enableDuration(row); + expect(row.get('timePeriodAgo')?.disabled).toBeTrue(); + expect(row.get('timePeriodUnit')?.disabled).toBeTrue(); + expect(() => component.enableDuration()).not.toThrow(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('fetches history and patches rows', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + MedicationHistory: { + medicationHistoryList: [ + { + currentMedication: 'a', + timePeriodAgo: 2, + timePeriodUnit: 'Days', + }, + { + currentMedication: 'b', + timePeriodAgo: null, + timePeriodUnit: null, + }, + ], + }, + }, + }) + ); + master$.next({ m: 1 }); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(list().length).toBe(2); + expect(list().at(0).get('timePeriodAgo')?.enabled).toBeTrue(); + expect(list().at(0).value.timePeriodUnit).toBe('Days'); + expect(list().at(1).get('timePeriodAgo')?.disabled).toBeTrue(); + expect(list().at(1).get('currentMedication')?.value).toBe('b'); + expect(list().dirty).toBeTrue(); + expect(list().touched).toBeTrue(); + }); + + it('ignores responses without MedicationHistory', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of({ statusCode: 500 })); + master$.next({ m: 1 }); + expect(component.medicationHistoryData).toBeUndefined(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/menstrual-history/menstrual-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/menstrual-history/menstrual-history.component.spec.ts index 7d962f5e..f4f4b8ba 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/menstrual-history/menstrual-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/menstrual-history/menstrual-history.component.spec.ts @@ -20,55 +20,278 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { MenstrualHistoryComponent } from './menstrual-history.component'; +const CONTROLS = [ + 'menstrualCycleStatus', + 'menstrualCycleStatusID', + 'regularity', + 'cycleLength', + 'menstrualCyclelengthID', + 'menstrualFlowDurationID', + 'bloodFlowDuration', + 'menstrualProblemID', + 'problemName', + 'menstrualProblemList', + 'lMPDate', +]; + describe('MenstrualHistoryComponent', () => { let component: MenstrualHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [MenstrualHistoryComponent], - }).compileComponents(); - })); + const master = { + menstrualCycleStatus: [{ name: 'Amenorrhea' }, { name: 'Active' }], + menstrualCycleLengths: [{ menstrualCycleRange: '21-35' }], + menstrualCycleBloodFlowDuration: [{ menstrualCycleRange: '3-5' }], + menstrualProblem: [{ problemName: 'None' }, { problemName: 'Pain' }], + }; - beforeEach(() => { + async function setup(mode: string, visitCategory: string) { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [MenstrualHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(MenstrualHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(MenstrualHistoryComponent); component = fixture.componentInstance; + const g: any = {}; + CONTROLS.forEach(c => (g[c] = new FormControl(null))); + form = new FormGroup(g); + component.menstrualHistoryForm = form; + component.mode = mode; + component.visitCategory = visitCategory; fixture.detectChanges(); + } + + describe('General OPD, new mode', () => { + beforeEach(async () => setup('new', 'General OPD')); + + it('initialises dates and language', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.today instanceof Date).toBeTrue(); + const diff = + component.today.getTime() - component.minimumLMPDate.getTime(); + expect(diff).toBe(365 * 24 * 60 * 60 * 1000); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ignores incomplete master data', () => { + master$.next({ menstrualCycleStatus: [] }); + expect(component.masterData).toBeUndefined(); + }); + + it('enables LMP date for non-ANC visits', () => { + form.get('lMPDate')?.disable(); + master$.next(master); + expect(component.masterData).toBe(master); + expect(form.get('lMPDate')?.enabled).toBeTrue(); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('ngOnDestroy unsubscribes', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.generalHistorySubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('checkMenstrualCycleStatus clears fields including LMP', () => { + spyOn(console, 'log'); + form.patchValue({ regularity: 'R', cycleLength: 'x', lMPDate: 'd' }); + component.checkMenstrualCycleStatus(); + expect(form.value.regularity).toBeNull(); + expect(form.value.cycleLength).toBeNull(); + expect(form.value.lMPDate).toBeNull(); + }); + + it('getters read controls', () => { + form.patchValue({ menstrualCycleStatus: 'S', lMPDate: 'L' }); + expect(component.menstrualCycleStatus).toBe('S'); + expect(component.lMPDate).toBe('L'); + component.menstrualHistoryForm = undefined as any; + expect(component.menstrualCycleStatus).toBeUndefined(); + }); + + it('resetOtherMenstrualProblems flags None and Other', () => { + component.resetOtherMenstrualProblems(); + expect(component.isNoneSelected).toBeFalse(); + expect(component.isOtherSelected).toBeFalse(); + form.patchValue({ menstrualProblemList: [{ problemName: 'None' }] }); + component.resetOtherMenstrualProblems(); + expect(component.isNoneSelected).toBeTrue(); + expect(component.isOtherSelected).toBeFalse(); + form.patchValue({ menstrualProblemList: [{ problemName: 'Pain' }] }); + component.resetOtherMenstrualProblems(); + expect(component.isNoneSelected).toBeFalse(); + expect(component.isOtherSelected).toBeTrue(); + }); + + describe('getPreviousMenstrualHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousMenstrualHistory.and.returnValue( + of({ statusCode: 200, data: payload }) + ); + component.getPreviousMenstrualHistory(); + expect(nurse.getPreviousMenstrualHistory).toHaveBeenCalledWith( + '11', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.Previousmenstrualhistory + .previousmenstrualhistory, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousMenstrualHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousMenstrualHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + }); + it('alerts on non-200', () => { + nurse.getPreviousMenstrualHistory.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPreviousMenstrualHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousMenstrualHistory.and.returnValue(throwingObs()); + component.getPreviousMenstrualHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); + }); + + describe('ANC visit', () => { + beforeEach(async () => setup('new', 'ANC')); + + it('sets Amenorrhea and disables LMP date', () => { + master$.next(master); + expect(form.value.menstrualCycleStatus).toEqual({ + name: 'Amenorrhea', + }); + expect(form.get('lMPDate')?.disabled).toBeTrue(); + }); + + it('checkMenstrualCycleStatus keeps LMP date', () => { + spyOn(console, 'log'); + form.patchValue({ regularity: 'R', lMPDate: 'keep' }); + component.checkMenstrualCycleStatus(); + expect(form.value.regularity).toBeNull(); + expect(form.value.lMPDate).toBe('keep'); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view', 'General OPD')); + + it('fetches history and maps master values', () => { + spyOn(console, 'log'); + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + MenstrualHistory: { + menstrualCycleStatus: 'Active', + cycleLength: '21-35', + bloodFlowDuration: '3-5', + menstrualProblemList: [{ problemName: 'Pain' }], + lMPDate: '2024-01-02T00:00:00.000Z', + regularity: 'Regular', + }, + }, + }) + ); + master$.next(master); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.menstrualCycleStatus).toEqual({ name: 'Active' }); + expect(form.value.cycleLength).toBe(master.menstrualCycleLengths[0]); + expect(form.value.bloodFlowDuration).toBe( + master.menstrualCycleBloodFlowDuration[0] + ); + expect(form.value.menstrualProblemList).toEqual([ + master.menstrualProblem[1], + ]); + expect(form.value.lMPDate instanceof Date).toBeTrue(); + expect(form.value.regularity).toBe('Regular'); + expect(component.isOtherSelected).toBeTrue(); + }); + + it('handles missing problem list', () => { + spyOn(console, 'log'); + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { MenstrualHistory: { lMPDate: null } } }) + ); + master$.next(master); + expect(form.value.menstrualProblemList).toEqual([]); + expect(component.isNoneSelected).toBeFalse(); + }); + + it('ignores non-200', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of({ statusCode: 500 })); + master$.next(master); + expect(form.value.regularity).toBeNull(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/other-vaccines/other-vaccines.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/other-vaccines/other-vaccines.component.spec.ts index 232d8988..d5256d9f 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/other-vaccines/other-vaccines.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/other-vaccines/other-vaccines.component.spec.ts @@ -20,55 +20,344 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { OtherVaccinesComponent } from './other-vaccines.component'; describe('OtherVaccinesComponent', () => { let component: OtherVaccinesComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; + let BCG: any, MMR: any, OTHER: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [OtherVaccinesComponent], - }).compileComponents(); - })); + function masterData() { + BCG = { vaccineName: 'BCG', sctCode: 'b1', sctTerm: 'bt' }; + MMR = { vaccineName: 'MMR', sctCode: null, sctTerm: null }; + OTHER = { vaccineName: 'Other', sctCode: null }; + return { vaccineMasterData: [BCG, MMR, OTHER] }; + } - beforeEach(() => { + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [OtherVaccinesComponent], + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(OtherVaccinesComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; fixture = TestBed.createComponent(OtherVaccinesComponent); component = fixture.componentInstance; + form = new FormGroup({ otherVaccines: new FormArray([]) }); + component.otherVaccinesForm = form; + component.mode = mode; + component.visitCategory = 'PNC'; fixture.detectChanges(); + } + + const list = () => form.controls['otherVaccines'] as FormArray; + const names = (a: any[]) => a.map(x => x.vaccineName); + + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('initialises language and beneficiary', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + ben$.next({ ageVal: 3 }); + expect(component.beneficiary).toEqual({ ageVal: 3 }); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(list().length).toBe(0); + }); + + it('ngOnDestroy unsubscribes all', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const spies = [ + component.nurseMasterDataSubscription, + component.generalHistorySubscription, + component.beneficiaryDetailSubscription, + ].map(s => spyOn(s, 'unsubscribe')); + component.ngOnDestroy(); + spies.forEach(s => expect(s).toHaveBeenCalled()); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('getOtherVaccines returns controls or null', () => { + expect(component.getOtherVaccines()).toEqual([]); + component.otherVaccinesForm = new FormGroup({ + otherVaccines: new FormControl(null), + }); + expect(component.getOtherVaccines()).toBeNull(); + }); + + it('addOtherVaccine without master data pushes empty select list', () => { + component.vaccineMasterData = null as any; + component.addOtherVaccine(); + expect(component.vaccineSelectList).toEqual([[]]); + expect(list().length).toBe(1); + }); + + describe('with master data', () => { + beforeEach(() => master$.next(masterData())); + + it('adds first row with all vaccines', () => { + expect(component.masterData.vaccineMasterData.length).toBe(3); + expect(list().length).toBe(1); + expect(component.vaccineSelectList[0].length).toBe(3); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('next row excludes selected (but not Other)', () => { + list().at(0).patchValue({ vaccineName: BCG }); + component.addOtherVaccine(); + expect(names(component.vaccineSelectList[1])).toEqual(['MMR', 'Other']); + list().at(1).patchValue({ vaccineName: OTHER }); + component.addOtherVaccine(); + expect(names(component.vaccineSelectList[2])).toEqual(['MMR', 'Other']); + }); + + it('filterOtherVaccineList patches sct codes and removes from others', () => { + component.addOtherVaccine(); + const row = list().at(0); + row.patchValue({ otherVaccineName: 'x' }); + component.filterOtherVaccineList({ value: BCG }, 0, row); + expect(row.value.sctCode).toBe('b1'); + expect(row.value.sctTerm).toBe('bt'); + expect(row.value.otherVaccineName).toBeNull(); + expect(component.vaccineSelectList[1]).not.toContain(BCG); + expect(component.previousSelectedVaccineList[0]).toBe(BCG); + }); + + it('null sct code clears codes and previous value is restored', () => { + component.addOtherVaccine(); + const row = list().at(0); + component.filterOtherVaccineList({ value: BCG }, 0, row); + component.filterOtherVaccineList({ value: MMR }, 0, row); + expect(row.value.sctCode).toBeNull(); + expect(row.value.sctTerm).toBeNull(); + expect(component.vaccineSelectList[1]).toContain(BCG); + expect(component.vaccineSelectList[1]).not.toContain(MMR); + }); + + it('Other leaves form untouched and is not restored twice', () => { + component.addOtherVaccine(); + const row = list().at(0); + row.patchValue({ sctCode: 'keep' }); + component.filterOtherVaccineList({ value: OTHER }, 0, row); + expect(row.value.sctCode).toBe('keep'); + component.filterOtherVaccineList({ value: BCG }, 0); + expect( + component.vaccineSelectList[1].filter((v: any) => v === OTHER).length + ).toBe(1); + }); + + describe('removeOtherVaccine', () => { + it('does nothing when cancelled', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeOtherVaccine(0, list().at(0)); + expect(list().length).toBe(1); + }); + + it('resets the only row', () => { + const row = list().at(0); + row.patchValue({ vaccineName: BCG }); + component.removeOtherVaccine(0, row); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(row.value.vaccineName).toBeNull(); + }); + + it('removes row and restores vaccine elsewhere', () => { + component.addOtherVaccine(); + component.filterOtherVaccineList({ value: MMR }, 1, list().at(1)); + component.removeOtherVaccine(1, list().at(1)); + expect(list().length).toBe(1); + expect(component.vaccineSelectList.length).toBe(1); + expect(component.vaccineSelectList[0]).toContain(MMR); + expect(form.dirty).toBeTrue(); + }); + + it('removes row without selection or form', () => { + component.addOtherVaccine(); + component.removeOtherVaccine(1); + expect(list().length).toBe(1); + }); + }); + + it('handleOtherVaccinesData throws for non-null vaccine (passes object, reads .value)', () => { + component.otherVaccineData = { + childOptionalVaccineList: [{ vaccineName: 'BCG' }], + }; + expect(() => component.handleOtherVaccinesData()).toThrowError( + TypeError + ); + }); + }); + + describe('getPreviousOtherVaccineDetails', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousOtherVaccines.and.returnValue( + of({ statusCode: 200, data: payload }) + ); + component.getPreviousOtherVaccineDetails(); + expect(nurse.getPreviousOtherVaccines).toHaveBeenCalledWith( + '11', + 'PNC' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { dataList: payload, title: LANGUAGE_EN.common.prevVaccine }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousOtherVaccines.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousOtherVaccineDetails(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + }); + it('alerts on non-200', () => { + nurse.getPreviousOtherVaccines.and.returnValue(of({ statusCode: 500 })); + component.getPreviousOtherVaccineDetails(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousOtherVaccines.and.returnValue(throwingObs()); + component.getPreviousOtherVaccineDetails(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); + + describe('validateAge', () => { + it('rejects age above beneficiary age', () => { + ben$.next({ ageVal: 2 }); + const g = new FormGroup({ actualReceivingAge: new FormControl(5) }); + component.validateAge(g); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.ageOfReceivingVaccine + ); + expect(g.value.actualReceivingAge).toBeNull(); + }); + it('accepts valid age or missing beneficiary', () => { + const g = new FormGroup({ actualReceivingAge: new FormControl(5) }); + component.validateAge(g); + ben$.next({ ageVal: 10 }); + component.validateAge(g); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(g.value.actualReceivingAge).toBe(5); + }); + }); + + it('sortOtherVaccineList sorts with equal items', () => { + const l = [ + { vaccineName: 'b' }, + { vaccineName: 'a' }, + { vaccineName: 'b' }, + ]; + component.sortOtherVaccineList(l); + expect(names(l)).toEqual(['a', 'b', 'b']); + }); + + it('checkValidity', () => { + expect( + component.checkValidity({ value: { vaccineName: 'a' } }) + ).toBeTrue(); + expect( + component.checkValidity({ + value: { + vaccineName: 'a', + actualReceivingAge: 1, + receivedFacilityName: 'f', + }, + }) + ).toBeFalse(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('fetches history and adds a row per entry', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + childOptionalVaccineHistory: { + childOptionalVaccineList: [ + { vaccineName: null }, + { vaccineName: null }, + ], + }, + }, + }) + ); + master$.next(masterData()); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(component.otherVaccineData).toBeDefined(); + expect(list().length).toBe(2); + }); + + it('ignores response without vaccine history', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + master$.next(masterData()); + expect(component.otherVaccineData).toBeUndefined(); + expect(list().length).toBe(1); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/past-history/past-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/past-history/past-history.component.spec.ts index f0ba1930..afb7230d 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/past-history/past-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/past-history/past-history.component.spec.ts @@ -20,55 +20,609 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MaterialModule } from 'src/app/app-modules/core/material.module'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { PreviousDetailsComponent } from '../../../../core/components/previous-details/previous-details.component'; import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - +import { ConfirmationService } from '../../../../core/services/confirmation.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { PastHistoryComponent } from './past-history.component'; describe('PastHistoryComponent', () => { let component: PastHistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let tracking: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + + const illnesses = () => [ + { illnessType: 'Asthma' }, + { illnessType: 'Diabetes' }, + { illnessType: 'Nil' }, + { illnessType: 'None' }, + { illnessType: 'Other' }, + ]; + const surgeries = () => [ + { surgeryType: 'Appendectomy' }, + { surgeryType: 'Cesarean Section' }, + { surgeryType: 'Nil' }, + { surgeryType: 'None' }, + { surgeryType: 'Other' }, + ]; + const masterData = () => ({ + illnessTypes: illnesses(), + surgeryTypes: surgeries(), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + const illnessArr = () => + component.pastHistoryForm.controls['pastIllness'] as FormArray; + const surgeryArr = () => + component.pastHistoryForm.controls['pastSurgery'] as FormArray; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject({ + age: '30 Years', + genderName: 'Female', + ageVal: 30, + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [PastHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + FormBuilder, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(PastHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + tracking = TestBed.inject(AmritTrackingService); + component.pastHistoryForm = new FormGroup({ + pastIllness: new FormArray([]), + pastSurgery: new FormArray([]), + }); + spyOn(console, 'log'); + }); + + afterEach(() => component.ngOnDestroy()); + + describe('initialisation', () => { + it('sets language and beneficiary, ignores null beneficiary', () => { + fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.beneficiary.ageVal).toBe(30); + ben$.next(null); + expect(component.beneficiary.ageVal).toBe(30); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ignores incomplete master data', () => { + fixture.detectChanges(); + master$.next({ illnessTypes: illnesses() }); + expect(illnessArr().length).toBe(0); + expect(component.illnessMasterData).toBeUndefined(); + }); + + it('keeps female surgeries for adult females and renders rows', () => { + fixture.detectChanges(); + master$.next(masterData()); + fixture.detectChanges(); + expect(component.surgeryMasterData.length).toBe(5); + expect(illnessArr().length).toBe(1); + expect(surgeryArr().length).toBe(1); + expect(component.pastIllnessSelectList[0].length).toBe(5); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + expect( + fixture.nativeElement.querySelectorAll('mat-select').length + ).toBeGreaterThan(0); + }); + + it('removes female-only surgeries for males', () => { + ben$.next({ age: '30 Years', genderName: 'Male', ageVal: 30 }); + fixture.detectChanges(); + master$.next(masterData()); + expect( + component.surgeryMasterData.map((s: any) => s.surgeryType) + ).not.toContain('Cesarean Section'); + expect(component.filteredSurgeryMasterData.length).toBe(4); + }); + + it('removes female-only surgeries for minors', () => { + ben$.next({ age: '15 Years', genderName: 'Female', ageVal: 15 }); + fixture.detectChanges(); + master$.next(masterData()); + expect(component.surgeryMasterData.length).toBe(4); + }); + + it('getPastIllness / getPastSurgery return null when not FormArray', () => { + fixture.detectChanges(); + master$.next(masterData()); + expect(component.getPastIllness()?.length).toBe(1); + expect(component.getPastSurgery()?.length).toBe(1); + component.pastHistoryForm = new FormGroup({}); + expect(component.getPastIllness()).toBeNull(); + expect(component.getPastSurgery()).toBeNull(); + }); + }); + + describe('view mode', () => { + it('loads illness and surgery history and patches rows', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + PastHistory: { + pastIllness: [ + { + illnessType: 'Asthma', + timePeriodAgo: 2, + timePeriodUnit: 'Years', + }, + { + illnessType: 'Unknown', + timePeriodAgo: null, + timePeriodUnit: null, + }, + { illnessType: null }, + ], + pastSurgery: [ + { + surgeryType: 'Appendectomy', + timePeriodAgo: 1, + timePeriodUnit: 'Years', + }, + { + surgeryType: 'Unknown', + timePeriodAgo: null, + timePeriodUnit: null, + }, + { surgeryType: null }, + ], + }, + }, + }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next(masterData()); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(illnessArr().length).toBe(3); + expect(surgeryArr().length).toBe(3); + const i0 = illnessArr().at(0); + expect(i0.value.illnessType.illnessType).toBe('Asthma'); + expect(i0.get('timePeriodAgo')?.enabled).toBeTrue(); + expect(i0.dirty).toBeTrue(); + expect(illnessArr().at(1).get('timePeriodAgo')?.disabled).toBeTrue(); + const s0 = surgeryArr().at(0); + expect(s0.value.surgeryType.surgeryType).toBe('Appendectomy'); + expect(s0.get('timePeriodUnit')?.enabled).toBeTrue(); + expect(component.previousSelectedSurgeryTypeList[0]).toEqual( + jasmine.objectContaining({ surgeryType: 'Appendectomy' }) + ); + }); + + it('handles history without lists, empty and failed responses', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { PastHistory: {} } }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next(masterData()); + expect(component.pastHistoryData).toEqual({}); + expect(illnessArr().length).toBe(1); + + component.pastHistoryData = undefined; + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('b1', 'v1'); + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getGeneralHistory('b1', 'v1'); + expect(component.pastHistoryData).toBeUndefined(); + component.handlePastHistoryIllnessData(); + component.handlePastHistorySurgeryData(); + expect(surgeryArr().length).toBe(1); + }); + }); + + describe('illness interactions', () => { + let m: any[]; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + m = component.illnessMasterData; + }); + + it('addPastIllness excludes selected items and None/Nil', () => { + illnessArr().at(0).patchValue({ illnessType: m[0] }); + component.addPastIllness(); + expect( + component.pastIllnessSelectList[1].map((x: any) => x.illnessType) + ).toEqual(['Diabetes', 'Other']); + illnessArr().at(1).patchValue({ illnessType: m[4] }); + component.addPastIllness(); + expect( + component.pastIllnessSelectList[2].map((x: any) => x.illnessType) + ).toEqual(['Diabetes', 'Other']); + component.filteredIllnessMasterData = undefined; + component.addPastIllness(); + expect(component.pastIllnessSelectList.length).toBe(3); + expect(illnessArr().length).toBe(4); + }); + + it('filterPastIllnessType moves selection between lists and enables duration', () => { + component.addPastIllness(); + const row = illnessArr().at(0); + row.patchValue({ otherIllnessType: 'x' }); + component.filterPastIllnessType(m[0], 0, row); + expect(row.value.otherIllnessType).toBeNull(); + expect(row.get('timePeriodAgo')?.enabled).toBeTrue(); + expect(component.pastIllnessSelectList[1]).not.toContain(m[0]); + component.filterPastIllnessType(m[1], 0, row); + expect(component.pastIllnessSelectList[1][0]).toBe(m[0]); + expect(component.previousSelectedIllnessTypeList[0]).toBe(m[1]); + }); + + it('filterPastIllnessType with Other keeps other text; previous Other not returned', () => { + component.addPastIllness(); + const row = illnessArr().at(0); + row.patchValue({ otherIllnessType: 'custom' }); + component.filterPastIllnessType(m[4], 0, row); + expect(row.value.otherIllnessType).toBe('custom'); + const len = component.pastIllnessSelectList[1].length; + component.filterPastIllnessType(m[1], 0); + expect(component.pastIllnessSelectList[1].length).toBe(len - 1); + }); + + it('filterPastIllnessType None/Nil collapses other rows', () => { + component.addPastIllness(); + component.addPastIllness(); + component.previousSelectedIllnessTypeList = [undefined, m[0], undefined]; + component.filterPastIllnessType(m[3], 0, illnessArr().at(0)); + expect(illnessArr().length).toBe(1); + expect(component.pastIllnessSelectList.length).toBe(1); + expect(component.previousSelectedIllnessTypeList.length).toBe(1); + component.filterPastIllnessType(m[2], 0); + expect(illnessArr().length).toBe(1); + }); + + it('filterPastIllnessTypeInDoctor mirrors selection logic', () => { + component.addPastIllness(); + const row = illnessArr().at(0) as FormGroup; + row.patchValue({ otherIllnessType: 'x' }); + component.filterPastIllnessTypeInDoctor(m[0], 0, row); + expect(row.value.otherIllnessType).toBeNull(); + expect(component.pastIllnessSelectList[1]).not.toContain(m[0]); + component.filterPastIllnessTypeInDoctor(m[1], 0); + expect(component.pastIllnessSelectList[1]).toContain(m[0]); + component.filterPastIllnessTypeInDoctor(m[4], 0); + component.filterPastIllnessTypeInDoctor(m[0], 0); + expect(component.previousSelectedIllnessTypeList[0]).toBe(m[0]); + component.filterPastIllnessTypeInDoctor(m[3], 0); + expect(illnessArr().length).toBe(1); + expect(console.log).toHaveBeenCalled(); + }); + + it('removePastIllness cancelled does nothing', () => { + confirm.confirm.and.returnValue(of(false)); + component.removePastIllness(0, illnessArr().at(0)); + expect(illnessArr().length).toBe(1); + expect(component.pastHistoryForm.dirty).toBeFalse(); + }); + + it('removePastIllness on single row resets it', () => { + const row = illnessArr().at(0); + row.patchValue({ illnessType: m[0] }); + row.get('timePeriodAgo')?.enable(); + component.removePastIllness(0, row); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(row.value.illnessType).toBeNull(); + expect(row.get('timePeriodAgo')?.disabled).toBeTrue(); + // current behaviour: reset+disable recompute pristine, undoing markAsDirty + expect(component.pastHistoryForm.dirty).toBeFalse(); + }); + + it('removePastIllness of first row returns value and None to next list', () => { + component.addPastIllness(); + component.previousSelectedIllnessTypeList = [m[0], m[1]]; + component.pastIllnessSelectList = [[m[0], m[3]], [m[1]]]; + component.removePastIllness(0, illnessArr().at(0)); + expect(illnessArr().length).toBe(1); + expect(component.pastIllnessSelectList).toEqual([[m[0], m[1], m[3]]]); + expect(component.previousSelectedIllnessTypeList).toEqual([m[1]]); + }); + + it('removePastIllness of non-first Other row does not return it', () => { + component.addPastIllness(); + component.previousSelectedIllnessTypeList = [m[0], m[4]]; + component.pastIllnessSelectList = [[m[0]], [m[4]]]; + component.removePastIllness(1, illnessArr().at(1)); + expect(component.pastIllnessSelectList).toEqual([[m[0]]]); + }); + + it('removePastIllness first row without previous value or None', () => { + component.addPastIllness(); + component.previousSelectedIllnessTypeList = []; + component.pastIllnessSelectList = [[m[0]], [m[1]]]; + component.removePastIllness(0); + expect(component.pastIllnessSelectList).toEqual([[m[1]]]); + }); + + it('checkIllnessValidity', () => { + const g: any = component.initPastIllness(); + expect(component.checkIllnessValidity(g)).toBeTrue(); + g.get('timePeriodAgo').enable(); + g.get('timePeriodUnit').enable(); + g.patchValue({ + illnessType: m[0], + timePeriodAgo: 1, + timePeriodUnit: 'Days', + }); + expect(component.checkIllnessValidity(g)).toBeFalse(); + }); + + it('sortIllnessList sorts by illnessType', () => { + const arr = [ + { illnessType: 'z' }, + { illnessType: 'a' }, + { illnessType: 'a' }, + ]; + component.sortIllnessList(arr); + expect(arr.map(a => a.illnessType)).toEqual(['a', 'a', 'z']); + }); + }); + + describe('surgery interactions', () => { + let m: any[]; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + m = component.surgeryMasterData; + }); + + it('addPastSurgery excludes selected items and None/Nil', () => { + surgeryArr().at(0).patchValue({ surgeryType: m[0] }); + component.addPastSurgery(); + expect( + component.pastSurgerySelectList[1].map((x: any) => x.surgeryType) + ).toEqual(['Cesarean Section', 'Other']); + surgeryArr().at(1).patchValue({ surgeryType: m[4] }); + component.addPastSurgery(); + expect(component.pastSurgerySelectList[2].length).toBe(2); + component.filteredSurgeryMasterData = undefined; + component.addPastSurgery(); + expect(component.pastSurgerySelectList.length).toBe(3); + expect(surgeryArr().length).toBe(4); + }); + + it('filterPastSurgeryType moves selection and enables duration', () => { + component.addPastSurgery(); + const row = surgeryArr().at(0); + row.patchValue({ otherSurgeryType: 'x' }); + component.filterPastSurgeryType(m[0], 0, row); + expect(row.value.otherSurgeryType).toBeNull(); + expect(row.get('timePeriodAgo')?.enabled).toBeTrue(); + expect(component.pastSurgerySelectList[1]).not.toContain(m[0]); + component.filterPastSurgeryType(m[1], 0, row); + expect(component.pastSurgerySelectList[1][0]).toBe(m[0]); + row.patchValue({ otherSurgeryType: 'y' }); + component.filterPastSurgeryType(m[4], 0, row); + expect(row.value.otherSurgeryType).toBe('y'); + const len = component.pastSurgerySelectList[1].length; + component.filterPastSurgeryType(m[0], 0); + expect(component.pastSurgerySelectList[1].length).toBe(len - 1); + }); + + it('filterPastSurgeryType None/Nil collapses other rows', () => { + component.addPastSurgery(); + component.addPastSurgery(); + component.previousSelectedSurgeryTypeList = [undefined, m[0], undefined]; + component.filterPastSurgeryType(m[2], 0, surgeryArr().at(0)); + expect(surgeryArr().length).toBe(1); + expect(component.pastSurgerySelectList[0]).toContain(m[0]); + component.filterPastSurgeryType(m[3], 0); + expect(surgeryArr().length).toBe(1); + }); + + it('removePastSurgery cancelled does nothing', () => { + confirm.confirm.and.returnValue(of(false)); + component.removePastSurgery(0, surgeryArr().at(0)); + expect(component.pastHistoryForm.dirty).toBeFalse(); + }); + + it('removePastSurgery on single row resets it', () => { + const row = surgeryArr().at(0); + row.patchValue({ surgeryType: m[0] }); + row.markAsTouched(); + row.get('timePeriodUnit')?.enable(); + component.removePastSurgery(0, row); + expect(row.value.surgeryType).toBeNull(); + expect(row.get('timePeriodUnit')?.disabled).toBeTrue(); + expect(row.touched).toBeFalse(); + expect(component.pastHistoryForm.dirty).toBeFalse(); + }); + + it('removePastSurgery of first row returns value and Nil to next list', () => { + component.addPastSurgery(); + component.previousSelectedSurgeryTypeList = [m[0], m[1]]; + component.pastSurgerySelectList = [[m[0], m[2]], [m[1]]]; + component.removePastSurgery(0, surgeryArr().at(0)); + expect(component.pastSurgerySelectList).toEqual([[m[0], m[1], m[2]]]); + expect(surgeryArr().length).toBe(1); + }); + + it('removePastSurgery of Other row / without previous value', () => { + component.addPastSurgery(); + component.addPastSurgery(); + component.previousSelectedSurgeryTypeList = [m[0], m[4]]; + component.pastSurgerySelectList = [[m[0]], [m[4]], [m[1]]]; + component.removePastSurgery(1); + expect(component.pastSurgerySelectList).toEqual([[m[0]], [m[1]]]); + component.previousSelectedSurgeryTypeList = []; + component.removePastSurgery(0); + expect(component.pastSurgerySelectList).toEqual([[m[1]]]); + }); + + it('checkSurgeryValidity', () => { + const g: any = component.initPastSurgery(); + expect(component.checkSurgeryValidity(g)).toBeTrue(); + g.get('timePeriodAgo').enable(); + g.get('timePeriodUnit').enable(); + g.patchValue({ + surgeryType: m[0], + timePeriodAgo: 1, + timePeriodUnit: 'Days', + }); + expect(component.checkSurgeryValidity(g)).toBeFalse(); + expect(component.checkSurgeryValidity(new FormGroup({}))).toBeTrue(); + }); + + it('sortSurgeryList sorts by surgeryType', () => { + const arr = [ + { surgeryType: 'b' }, + { surgeryType: 'a' }, + { surgeryType: 'b' }, + ]; + component.sortSurgeryList(arr); + expect(arr.map(a => a.surgeryType)).toEqual(['a', 'b', 'b']); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('misc', () => { + beforeEach(() => fixture.detectChanges()); + + it('validateDuration alerts when exceeding age', () => { + const g: any = component.initPastIllness(); + g.get('timePeriodAgo').enable(); + g.get('timePeriodUnit').enable(); + g.patchValue({ timePeriodAgo: 50, timePeriodUnit: 'Years' }); + component.validateDuration(g); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.durationGreaterThanAge + ); + expect(g.value.timePeriodAgo).toBeNull(); + }); + + it('validateDuration enables/disables unit', () => { + const g: any = component.initPastIllness(); + g.get('timePeriodAgo').enable(); + g.patchValue({ timePeriodAgo: 3 }); + component.validateDuration(g); + expect(g.get('timePeriodUnit').enabled).toBeTrue(); + g.patchValue({ timePeriodAgo: 2, timePeriodUnit: 'Days' }); + component.validateDuration(g); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(g.value.timePeriodUnit).toBe('Days'); + g.patchValue({ timePeriodAgo: null }); + component.validateDuration(g); + expect(g.get('timePeriodUnit').disabled).toBeTrue(); + }); + + it('getPreviousPastHistory opens dialog when data exists', () => { + component.visitCategory = 'General OPD'; + const data = { data: [{}] }; + nurse.getPreviousPastHistory.and.returnValue( + of({ statusCode: 200, data }) + ); + component.getPreviousPastHistory(); + expect(nurse.getPreviousPastHistory).toHaveBeenCalledWith( + 'b1', + 'General OPD' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: data, + title: LANGUAGE_EN.historyData.Previousillness.previouspasthistory, + }, + }); + }); + + it('getPreviousPastHistory alerts on empty, failure and error', () => { + nurse.getPreviousPastHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousPastHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + nurse.getPreviousPastHistory.and.returnValue(of({ statusCode: 500 })); + component.getPreviousPastHistory(); + nurse.getPreviousPastHistory.and.returnValue(throwingObs()); + component.getPreviousPastHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + expect(confirm.alert).toHaveBeenCalledTimes(3); + }); + + it('trackFieldInteraction delegates to tracking service', () => { + component.trackFieldInteraction('Illness'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Illness', + 'Past History' + ); + }); + + it('ngOnDestroy unsubscribes and tolerates missing subscriptions', () => { + component.getGeneralHistory('b1', 'v1'); + const subs = [ + component.nurseMasterDataSubscription, + component.generalHistorySubscription, + component.beneficiaryDetailSubscription, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/past-obsteric-history/past-obsteric-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/past-obsteric-history/past-obsteric-history.component.spec.ts index 42cda360..a165c3da 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/past-obsteric-history/past-obsteric-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/past-obsteric-history/past-obsteric-history.component.spec.ts @@ -20,55 +20,696 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { SimpleChange } from '@angular/core'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MaterialModule } from 'src/app/app-modules/core/material.module'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { PastObstericHistoryComponent } from './past-obsteric-history.component'; describe('PastObstericHistoryComponent', () => { let component: PastObstericHistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let session: any; + let master$: BehaviorSubject; + + const masterData = () => ({ + pregComplicationTypes: [ + { pregComplicationType: 'Anaemia' }, + { pregComplicationType: 'Other' }, + ], + pregDuration: [{ durationType: 'Full Term' }], + deliveryTypes: [ + { deliveryType: 'Normal Delivery' }, + { deliveryType: 'Assisted Delivery' }, + { deliveryType: 'Cesarean Section (LSCS)' }, + ], + deliveryPlaces: [ + { deliveryPlace: 'Home-Supervised' }, + { deliveryPlace: 'Health Facility' }, + ], + deliveryComplicationTypes: [{ deliveryComplicationType: 'PPH' }], + postpartumComplicationTypes: [{ postpartumComplicationType: 'Sepsis' }], + pregOutcomes: [ + { pregOutcome: 'Live Birth' }, + { pregOutcome: 'Abortion' }, + { pregOutcome: 'Stillbirth' }, + ], + newBornComplications: [{ complicationValue: 'Jaundice' }], + typeOfAbortion: [{ complicationValue: 'Induced' }], + serviceFacilities: [{ facilityName: 'PHC' }], + postAbortionComplications: [ + { complicationValue: 'Bleeding' }, + { complicationValue: 'None' }, + ], + }); + + const makeForm = () => + new FormGroup({ + totalNoOfPreg: new FormControl(null), + vanID: new FormControl(7), + parkingPlaceID: new FormControl(8), + complicationPregList: new FormArray([]), + pastObstericHistoryList: new FormArray([]), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + const rows = () => + component.pastObstericHistoryForm.controls[ + 'pastObstericHistoryList' + ] as FormArray; + const compl = () => + component.pastObstericHistoryForm.controls[ + 'complicationPregList' + ] as FormArray; + + beforeEach(async () => { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [PastObstericHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { + visitID: 'v1', + beneficiaryRegID: 'b1', + serviceLineDetails: JSON.stringify({ + vanID: 7, + parkingPlaceID: 8, + }), + }, + }), + FormBuilder, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(PastObstericHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + component.pastObstericHistoryForm = makeForm(); + spyOn(console, 'log'); + }); + + describe('initialisation', () => { + it('renders, sets language and loads master data', () => { + fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(fixture.nativeElement.textContent).toBeDefined(); + master$.next(masterData()); + expect(component.masterData.pregOutcomes.length).toBe(3); + expect(component.selectDeliveryTypes.length).toBe(3); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('list getters return controls or null', () => { + fixture.detectChanges(); + expect(component.getComplicationPregList()).toEqual([]); + expect(component.getPastObstericHistoryList()).toEqual([]); + const saved = component.pastObstericHistoryForm; + component.pastObstericHistoryForm = new FormGroup({}); + expect(component.getComplicationPregList()).toBeNull(); + expect(component.getPastObstericHistoryList()).toBeNull(); + component.pastObstericHistoryForm = saved; + }); + + it('ngOnChanges builds constraints per existing row', () => { + rows().push(component.formUtility.initPastObstericHistory(1)); + rows().push(component.formUtility.initPastObstericHistory(2)); + component.ngOnChanges({ + pastObstericHistoryForm: new SimpleChange(null, {}, true), + }); + expect(component.complicationOptionConstraints.length).toBe(2); + expect( + component.complicationOptionConstraints[0].showAllPregComplication + ).toBeTrue(); + component.ngOnChanges({ mode: new SimpleChange(null, 'view', true) }); + expect(component.complicationOptionConstraints.length).toBe(2); + }); + }); + + describe('total number of pregnancies', () => { + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + }); + + it('creates complication checkboxes for non-ANC visits', () => { + component.visitCategory = 'General OPD'; + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 3 }); + expect(compl().length).toBe(3); + expect(component.totalNoOfPreg).toBe(3); + }); + + it('creates one less checkbox for ANC visits and ignores zero', () => { + component.visitCategory = 'ANC'; + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 3 }); + expect(compl().length).toBe(2); + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 0 }); + expect(compl().length).toBe(2); + }); + + it('shrinking removes checkboxes and the matching pregnancy rows', () => { + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 3 }); + component.togglePastObstericHistory({ checked: true }, 3); + component.togglePastObstericHistory({ checked: true }, 1); + expect(rows().length).toBe(2); + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 1 }); + expect(compl().length).toBe(1); + expect(rows().length).toBe(1); + expect(rows().at(0).value.pregOrder).toBe(1); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(component.pastObstericHistoryForm.dirty).toBeTrue(); + }); + + it('checkTotalPregnancy alerts only for zero in ANC/PNC', () => { + component.visitCategory = 'ANC'; + component.checkTotalPregnancy(0); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.totalNumberOfPastPregnancyFor + ' ', + 'ANC', + ' ' + LANGUAGE_EN.cannotBeZero + ); + component.visitCategory = 'PNC'; + component.checkTotalPregnancy(0); + component.checkTotalPregnancy(2); + component.visitCategory = 'General OPD'; + component.checkTotalPregnancy(0); + expect(confirm.alert).toHaveBeenCalledTimes(2); + }); + }); + + describe('toggle / remove', () => { + beforeEach(() => fixture.detectChanges()); + + it('toggle on adds row and constraint; toggle off for unknown order does nothing', () => { + component.togglePastObstericHistory({ checked: true }, 2); + expect(rows().length).toBe(1); + expect(rows().at(0).value.pregOrder).toBe(2); + expect(component.complicationOptionConstraints.length).toBe(1); + component.togglePastObstericHistory({ checked: false }, 5); + expect(confirm.confirm).not.toHaveBeenCalled(); + expect(component.findPastObstericHistory(2)).toBe(0); + expect(component.findPastObstericHistory(5)).toBe(-1); + }); + + it('toggle off removes row and saved history entry', () => { + component.pastObstericHistoryData = { + femaleObstetricHistoryList: [{ pregOrder: 1 }, { pregOrder: 2 }], + }; + component.togglePastObstericHistory({ checked: true }, 1); + component.togglePastObstericHistory({ checked: false }, 1); + expect(rows().length).toBe(0); + expect( + component.pastObstericHistoryData.femaleObstetricHistoryList + ).toEqual([{ pregOrder: 2 }]); + }); + + it('toggle off without saved history still removes row', () => { + component.pastObstericHistoryData = {}; + component.togglePastObstericHistory({ checked: true }, 1); + component.togglePastObstericHistory({ checked: false }, 1); + expect(rows().length).toBe(0); + }); + + it('cancelled remove re-checks the pregnancy checkbox', () => { + compl().push(new FormGroup({ value: new FormControl(false) })); + component.togglePastObstericHistory({ checked: true }, 1); + component.complicationOptionConstraints.push({}); + confirm.confirm.and.returnValue(of(false)); + component.removePastObstericHistory(0, {}, 1); + expect(rows().length).toBe(1); + expect(compl().at(0).value.value).toBeTrue(); + expect(component.complicationOptionConstraints.length).toBe(1); + }); + }); + + describe('complication helpers', () => { + let row: FormGroup; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + component.togglePastObstericHistory({ checked: true }, 1); + row = rows().at(0) as FormGroup; + }); + + const c = () => component.complicationOptionConstraints[0]; + + it('resetOtherPregnancyComplication', () => { + row.patchValue({ + pregComplicationList: [ + { pregComplicationType: 'Other' }, + { pregComplicationType: 'Anaemia' }, + ], + otherPregComplication: 'x', + }); + component.resetOtherPregnancyComplication(row, 0); + expect(c().showOtherPregnancyComplication).toBeTrue(); + expect(c().disableNonePregnancyComplication).toBeTrue(); + expect(c().showAllPregComplication).toBeFalse(); + expect(row.value.otherPregComplication).toBe('x'); + + row.patchValue({ + pregComplicationList: [{ pregComplicationType: 'None' }], + }); + component.resetOtherPregnancyComplication(row, 0); + expect(c().disableNonePregnancyComplication).toBeFalse(); + expect(row.value.otherPregComplication).toBeNull(); + + row.patchValue({ + pregComplicationList: [{ pregComplicationType: 'Nil' }], + }); + component.resetOtherPregnancyComplication(row, 0); + expect(c().disableNonePregnancyComplication).toBeFalse(); + + row.patchValue({ + pregComplicationList: [{ pregComplicationType: 'Anaemia' }], + }); + component.resetOtherPregnancyComplication(row, 0); + expect(c().disableNonePregnancyComplication).toBeTrue(); + + row.patchValue({ pregComplicationList: [] }); + component.resetOtherPregnancyComplication(row, 0); + expect(c().disableNonePregnancyComplication).toBeFalse(); + expect(c().showAllPregComplication).toBeTrue(); + }); + + it('resetOtherDeliveryComplication', () => { + row.patchValue({ + deliveryComplicationList: [ + { deliveryComplicationType: 'Other' }, + { deliveryComplicationType: 'PPH' }, + ], + otherDeliveryComplication: 'x', + }); + component.resetOtherDeliveryComplication(row, 0); + expect(c().showOtherDeliveryComplication).toBeTrue(); + expect(c().disableNoneDeliveryComplication).toBeTrue(); + expect(c().showAllDeliveryComplication).toBeFalse(); + expect(row.value.otherDeliveryComplication).toBe('x'); + + row.patchValue({ + deliveryComplicationList: [{ deliveryComplicationType: 'None' }], + }); + component.resetOtherDeliveryComplication(row, 0); + expect(c().disableNoneDeliveryComplication).toBeFalse(); + expect(row.value.otherDeliveryComplication).toBeNull(); + + row.patchValue({ + deliveryComplicationList: [{ deliveryComplicationType: 'Nil' }], + }); + component.resetOtherDeliveryComplication(row, 0); + expect(c().disableNoneDeliveryComplication).toBeFalse(); + + row.patchValue({ + deliveryComplicationList: [{ deliveryComplicationType: 'PPH' }], + }); + component.resetOtherDeliveryComplication(row, 0); + expect(c().disableNoneDeliveryComplication).toBeTrue(); + + row.patchValue({ deliveryComplicationList: [] }); + component.resetOtherDeliveryComplication(row, 0); + expect(c().showAllDeliveryComplication).toBeTrue(); + }); + + it('resetOtherPostpartumComplicationType', () => { + row.patchValue({ + postpartumComplicationList: [ + { postpartumComplicationType: 'Other' }, + { postpartumComplicationType: 'Sepsis' }, + ], + otherPostpartumCompType: 'x', + }); + component.resetOtherPostpartumComplicationType(row, 0); + expect(c().showOtherPostpartumComplication).toBeTrue(); + expect(c().disableNonePostpartumComplication).toBeTrue(); + expect(row.value.otherPostpartumCompType).toBe('x'); + + row.patchValue({ + postpartumComplicationList: [{ postpartumComplicationType: 'None' }], + }); + component.resetOtherPostpartumComplicationType(row, 0); + expect(c().disableNonePostpartumComplication).toBeFalse(); + expect(row.value.otherPostpartumCompType).toBeNull(); + + row.patchValue({ + postpartumComplicationList: [{ postpartumComplicationType: 'Nil' }], + }); + component.resetOtherPostpartumComplicationType(row, 0); + expect(c().disableNonePostpartumComplication).toBeFalse(); + + row.patchValue({ + postpartumComplicationList: [{ postpartumComplicationType: 'Sepsis' }], + }); + component.resetOtherPostpartumComplicationType(row, 0); + expect(c().disableNonePostpartumComplication).toBeTrue(); + + row.patchValue({ postpartumComplicationList: [] }); + component.resetOtherPostpartumComplicationType(row, 0); + expect(c().showAllPostpartumComplication).toBeTrue(); + }); + + it('resetPostComplicationType', () => { + row.patchValue({ + postAbortionComplication: [ + { complicationValue: 'Bleeding' }, + { complicationValue: 'Fever' }, + ], + }); + component.resetPostComplicationType(row, 0); + expect(c().disableNonePostComplication).toBeTrue(); + expect(c().showAllPostComplication).toBeFalse(); + row.patchValue({ + postAbortionComplication: [{ complicationValue: 'None' }], + }); + component.resetPostComplicationType(row, 0); + expect(c().disableNonePostComplication).toBeFalse(); + row.patchValue({ + postAbortionComplication: [{ complicationValue: 'Nil' }], + }); + component.resetPostComplicationType(row, 0); + expect(c().disableNonePostComplication).toBeFalse(); + row.patchValue({ + postAbortionComplication: [{ complicationValue: 'Bleeding' }], + }); + component.resetPostComplicationType(row, 0); + expect(c().disableNonePostComplication).toBeTrue(); + row.patchValue({ postAbortionComplication: [] }); + component.resetPostComplicationType(row, 0); + expect(c().showAllPostComplication).toBeTrue(); + }); + + it('resetOtherDeliveryPlace filters delivery types for home deliveries', () => { + row.patchValue({ deliveryPlace: { deliveryPlace: 'Home-Supervised' } }); + component.resetOtherDeliveryPlace(row); + expect( + component.selectDeliveryTypes.map((d: any) => d.deliveryType) + ).toEqual(['Normal Delivery']); + row.patchValue({ deliveryPlace: { deliveryPlace: 'Home-Unsupervised' } }); + component.resetOtherDeliveryPlace(row); + expect(component.selectDeliveryTypes.length).toBe(1); + row.patchValue({ + deliveryPlace: { deliveryPlace: 'Other' }, + otherDeliveryPlace: 'x', + }); + component.resetOtherDeliveryPlace(row); + expect(component.selectDeliveryTypes.length).toBe(3); + expect(row.value.otherDeliveryPlace).toBeNull(); + row.patchValue({ + deliveryPlace: { deliveryPlace: 'Health Facility' }, + otherDeliveryPlace: 'y', + }); + component.resetOtherDeliveryPlace(row); + expect(row.value.otherDeliveryPlace).toBe('y'); + }); + + it('resetOtherNewBornComplications clears other text only for Other', () => { + row.patchValue({ + newBornComplication: { complicationValue: 'Jaundice' }, + otherNewBornComplication: 'x', + }); + component.resetOtherNewBornComplications(row); + expect(row.value.otherNewBornComplication).toBe('x'); + row.patchValue({ newBornComplication: { complicationValue: 'Other' } }); + component.resetOtherNewBornComplications(row); + expect(row.value.otherNewBornComplication).toBeNull(); + }); + + it('trackComplication returns value or undefined', () => { + expect(component.trackComplication({ value: 3 }, 0)).toBe(3); + expect(component.trackComplication(null, 0)).toBeUndefined(); + }); + + it('checkPregnancyOutcome clears fields for abortion and stillbirth', () => { + row.patchValue({ + pregOutcome: { pregOutcome: 'Abortion' }, + deliveryPlace: { deliveryPlace: 'PHC' }, + newBornComplication: { complicationValue: 'Jaundice' }, + }); + component.checkPregnancyOutcome(row); + expect(row.value.deliveryPlace).toBeNull(); + expect(row.value.newBornComplication).toBeNull(); + + row.patchValue({ + pregOutcome: { pregOutcome: 'Stillbirth' }, + deliveryPlace: { deliveryPlace: 'PHC' }, + congenitalAnomalies: 'x', + }); + component.checkPregnancyOutcome(row); + expect(row.value.congenitalAnomalies).toBeNull(); + expect(row.value.deliveryPlace).toEqual({ deliveryPlace: 'PHC' }); + + row.patchValue({ + pregOutcome: { pregOutcome: 'Live Birth' }, + congenitalAnomalies: 'y', + }); + component.checkPregnancyOutcome(row); + expect(row.value.congenitalAnomalies).toBe('y'); + }); + + it('onAbortionType clears facility unless induced', () => { + row.patchValue({ typeofFacility: { facilityName: 'PHC' } }); + component.onAbortionType(row, 'Induced'); + expect(row.value.typeofFacility).toEqual({ facilityName: 'PHC' }); + component.onAbortionType(row, 'Spontaneous'); + expect(row.value.typeofFacility).toBeNull(); + }); + + it('checkDurationType validates 4..24 weeks', () => { + row.patchValue({ pregDuration: 30 }); + component.checkDurationType(row); + expect(row.value.pregDuration).toBeNull(); + row.patchValue({ pregDuration: 2 }); + component.checkDurationType(row); + expect(row.value.pregDuration).toBeNull(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pregnancyRange + ); + row.patchValue({ pregDuration: 12 }); + component.checkDurationType(row); + expect(row.value.pregDuration).toBe(12); + expect(confirm.alert).toHaveBeenCalledTimes(2); + }); + }); + + describe('previous history', () => { + beforeEach(() => fixture.detectChanges()); + + it('opens dialog when data exists', () => { + component.visitCategory = 'ANC'; + const data = { data: [{}] }; + nurse.getPreviousObstetricHistory.and.returnValue( + of({ statusCode: 200, data }) + ); + component.getPreviousObstetricHistory(); + expect(nurse.getPreviousObstetricHistory).toHaveBeenCalledWith( + 'b1', + 'ANC' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: data, + title: + LANGUAGE_EN.historyData.obstetrichistory.previousobstetrichistory, + }, + }); + }); + + it('alerts on empty, failure and error', () => { + nurse.getPreviousObstetricHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousObstetricHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pastHistoryNot + ); + nurse.getPreviousObstetricHistory.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPreviousObstetricHistory(); + nurse.getPreviousObstetricHistory.and.returnValue(throwingObs()); + component.getPreviousObstetricHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + expect(confirm.alert).toHaveBeenCalledTimes(3); + }); + }); + + describe('view mode', () => { + it('loads obstetric history and patches rows', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + FemaleObstetricHistory: { + totalNoOfPreg: 3, + femaleObstetricHistoryList: [ + { + pregOrder: 1, + pregComplicationList: [{ pregComplicationType: 'Anaemia' }], + durationType: 'Full Term', + deliveryType: 'Normal Delivery', + deliveryPlace: 'Health Facility', + deliveryComplicationList: [ + { deliveryComplicationType: 'PPH' }, + ], + postpartumComplicationList: [ + { postpartumComplicationType: 'Sepsis' }, + ], + pregOutcome: 'Live Birth', + newBornComplication: 'Jaundice', + }, + { + pregOrder: 2, + pregComplicationList: [], + deliveryComplicationList: [], + postpartumComplicationList: [], + pregOutcome: 'Abortion', + typeOfAbortionValue: 'Induced', + serviceFacilityValue: 'PHC', + postAbortionComplication: [{ complicationValue: 'Bleeding' }], + }, + { + pregOrder: 5, + pregComplicationList: [], + deliveryComplicationList: [], + postpartumComplicationList: [], + pregOutcome: 'Abortion', + postAbortionComplication: [], + }, + { pregOrder: null }, + ], + }, + }, + }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next(masterData()); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.pastObstericHistoryForm.value.totalNoOfPreg).toBe(3); + expect(compl().length).toBe(3); + expect(rows().length).toBe(3); + const r0 = rows().at(0).value; + expect(r0.pregComplicationList).toEqual([ + { pregComplicationType: 'Anaemia' }, + ]); + expect(r0.deliveryType).toEqual({ deliveryType: 'Normal Delivery' }); + expect(r0.pregOutcome).toEqual({ pregOutcome: 'Live Birth' }); + expect(r0.newBornComplication).toEqual({ complicationValue: 'Jaundice' }); + const r1 = rows().at(1).value; + expect(r1.abortionType).toEqual({ complicationValue: 'Induced' }); + expect(r1.typeofFacility).toEqual({ facilityName: 'PHC' }); + expect(r1.postAbortionComplication).toEqual([ + { complicationValue: 'Bleeding' }, + ]); + expect(compl().at(0).value.value).toBeTrue(); + expect(compl().at(1).value.value).toBeTrue(); + expect(compl().at(2).value.value).toBeNull(); + const c = component.complicationOptionConstraints; + expect(c[1].disableNonePostComplication).toBeTrue(); + expect(c[2].showAllPostComplication).toBeTrue(); + }); + + it('handles zero total and abortion without post-abortion list', () => { + component.pastObstericHistoryData = { + totalNoOfPreg: 0, + femaleObstetricHistoryList: [ + { + pregOrder: 1, + pregComplicationList: [], + deliveryComplicationList: [], + postpartumComplicationList: [], + pregOutcome: 'Abortion', + postAbortionComplication: null, + }, + ], + }; + fixture.detectChanges(); + component.masterData = masterData(); + expect(() => component.handlePastObstetricHistoryData()).toThrow(); + expect(rows().length).toBe(1); + expect(component.pastObstericHistoryForm.value.totalNoOfPreg).toBeNull(); + }); + + it('ignores empty or failed history', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.mode = 'view'; + fixture.detectChanges(); + master$.next(masterData()); + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('b1', 'v1'); + expect(component.pastObstericHistoryData).toBeUndefined(); + expect(rows().length).toBe(0); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('ngOnDestroy', () => { + it('unsubscribes, clears arrays and resets form with van details', () => { + fixture.detectChanges(); + component.getGeneralHistory('b1', 'v1'); + component.pastObstericHistoryForm.patchValue({ totalNoOfPreg: 2 }); + component.togglePastObstericHistory({ checked: true }, 1); + const subs = [ + component.nurseMasterDataSubscription, + component.totalNoofPregChangeSubs, + component.generalHistorySubscription, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + expect(rows().length).toBe(0); + expect(compl().length).toBe(0); + expect(component.pastObstericHistoryForm.value.vanID).toBe(7); + expect(component.pastObstericHistoryForm.value.parkingPlaceID).toBe(8); + expect(component.pastObstericHistoryForm.value.totalNoOfPreg).toBeNull(); + }); + + it('skips reset when no service line details and no subscriptions', () => { + session.removeItem('serviceLineDetails'); + component.pastObstericHistoryForm.patchValue({ vanID: 99 }); + component.ngOnDestroy(); + expect(component.pastObstericHistoryForm.value.vanID).toBe(99); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/perinatal-history/perinatal-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/perinatal-history/perinatal-history.component.spec.ts index 013d9d56..6f4bb94f 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/perinatal-history/perinatal-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/perinatal-history/perinatal-history.component.spec.ts @@ -20,55 +20,271 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; -import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { PerinatalHistoryComponent } from './perinatal-history.component'; +const CONTROLS = [ + 'placeOfDelivery', + 'otherPlaceOfDelivery', + 'typeOfDelivery', + 'complicationAtBirth', + 'otherComplicationAtBirth', + 'gestation', + 'birthWeight_kg', +]; + describe('PerinatalHistoryComponent', () => { let component: PerinatalHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PerinatalHistoryComponent], - }).compileComponents(); - })); + const master = { + deliveryTypes: [ + { deliveryTypeID: 1, deliveryType: 'Normal Delivery' }, + { deliveryTypeID: 2, deliveryType: 'Assisted Delivery' }, + { deliveryTypeID: 3, deliveryType: 'Cesarean Section (LSCS)' }, + ], + deliveryPlaces: [ + { deliveryPlaceID: 10, deliveryPlace: 'Home-Supervised' }, + { deliveryPlaceID: 11, deliveryPlace: 'Hospital' }, + ], + deliveryComplicationTypes: [{ complicationID: 5, complicationValue: 'X' }], + }; - beforeEach(() => { + async function setup(mode = 'new') { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PerinatalHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22' }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PerinatalHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + spyOn(console, 'log'); fixture = TestBed.createComponent(PerinatalHistoryComponent); component = fixture.componentInstance; + const g: any = {}; + CONTROLS.forEach(c => (g[c] = new FormControl(null))); + form = new FormGroup(g); + component.perinatalHistoryForm = form; + component.mode = mode; + component.visitCategory = 'PNC'; fixture.detectChanges(); + } + + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('sets language; master data sets delivery types', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.masterData).toBeUndefined(); + master$.next(master); + expect(component.selectDeliveryTypes).toBe(master.deliveryTypes); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('ngOnDestroy unsubscribes', () => { + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('1', '2'); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.generalHistorySubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); + component.nurseMasterDataSubscription = null; + component.generalHistorySubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + + it('checkWeight warns for >= 6 kg only', () => { + form.patchValue({ birthWeight_kg: 3 }); + component.checkWeight(3); + expect(confirm.alert).not.toHaveBeenCalled(); + form.patchValue({ birthWeight_kg: 6 }); + component.checkWeight(6); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.recheckValue + ); + }); + + describe('resetOtherPlaceOfDelivery', () => { + beforeEach(() => master$.next(master)); + + it('home delivery restricts types and enables delivery type', () => { + form.get('typeOfDelivery')?.disable(); + form.patchValue({ + placeOfDelivery: master.deliveryPlaces[0], + otherPlaceOfDelivery: 'x', + }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes).toEqual([ + master.deliveryTypes[0], + ]); + expect(form.value.otherPlaceOfDelivery).toBeNull(); + expect(form.get('typeOfDelivery')?.enabled).toBeTrue(); + }); + + it('Home-Unsupervised also restricts', () => { + form.patchValue({ + placeOfDelivery: { deliveryPlace: 'Home-Unsupervised' }, + }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes.length).toBe(1); + }); + + it('hospital keeps all types', () => { + form.patchValue({ placeOfDelivery: master.deliveryPlaces[1] }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes).toBe(master.deliveryTypes); + expect(component.placeOfDelivery).toBe(master.deliveryPlaces[1]); + }); + + it('empty place disables delivery type', () => { + form.patchValue({ placeOfDelivery: {} }); + component.resetOtherPlaceOfDelivery(); + expect(form.get('typeOfDelivery')?.disabled).toBeTrue(); + }); + }); + + it('resetOtherComplicationAtBirth clears other value', () => { + form.patchValue({ + complicationAtBirth: 'c', + otherComplicationAtBirth: 'o', + }); + component.resetOtherComplicationAtBirth(); + expect(form.value.otherComplicationAtBirth).toBeNull(); + expect(component.complicationAtBirth).toBe('c'); + }); + + describe('getPreviousPerinatalHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data', () => { + const payload = { data: [1] }; + nurse.getPreviousPerinatalHistory.and.returnValue( + of({ data: payload }) + ); + component.getPreviousPerinatalHistory(); + expect(nurse.getPreviousPerinatalHistory).toHaveBeenCalledWith( + '11', + 'PNC' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: + LANGUAGE_EN.historyData.Perinatalhistorydetails + .developmentalhistorydetails, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousPerinatalHistory.and.returnValue( + of({ data: { data: [] } }) + ); + component.getPreviousPerinatalHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + }); + it('alerts when data null', () => { + nurse.getPreviousPerinatalHistory.and.returnValue(of({ data: null })); + component.getPreviousPerinatalHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousPerinatalHistory.and.returnValue(throwingObs()); + component.getPreviousPerinatalHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode', () => { + beforeEach(async () => setup('view')); + + it('maps IDs to master objects and enables delivery type', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + PerinatalHistory: { + deliveryPlaceID: 11, + deliveryTypeID: 1, + complicationID: 5, + gestation: 'Term', + }, + }, + }) + ); + form.get('typeOfDelivery')?.disable(); + master$.next(master); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.placeOfDelivery).toBe(master.deliveryPlaces[1]); + expect(form.value.typeOfDelivery).toBe(master.deliveryTypes[0]); + expect(form.value.complicationAtBirth).toBe( + master.deliveryComplicationTypes[0] + ); + expect(form.value.gestation).toBe('Term'); + expect(form.get('typeOfDelivery')?.enabled).toBeTrue(); + }); + + it('disables delivery type when none recorded', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { PerinatalHistory: { gestation: 'P' } } }) + ); + master$.next(master); + expect(form.value.gestation).toBe('P'); + expect(form.get('typeOfDelivery')?.disabled).toBeTrue(); + }); + + it('ignores null PerinatalHistory', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { PerinatalHistory: null } }) + ); + master$.next(master); + expect(component.perinatalHistoryData).toBeUndefined(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/personal-history/personal-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/personal-history/personal-history.component.spec.ts index d9cae3e8..8957aa18 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/personal-history/personal-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/personal-history/personal-history.component.spec.ts @@ -20,55 +20,886 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../../core/material.module'; - -import { GeneralUtils } from '../../../shared/utility'; - -import { ConfirmationService } from '../../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../../shared/services'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { MaterialModule } from 'src/app/app-modules/core/material.module'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { AllergenSearchComponent } from 'src/app/app-modules/core/components/allergen-search/allergen-search.component'; import { BeneficiaryDetailsService } from '../../../../core/services/beneficiary-details.service'; - -import { BeneficiaryDetailsServiceStub } from '../../../../core/mocks/beneficiary-details-service-stub'; -import { MasterdataServiceStub } from '../../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../../shared/mocks/doctor-service-stub'; - -import * as data from '../../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - +import { ConfirmationService } from '../../../../core/services/confirmation.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; import { GeneralPersonalHistoryComponent } from './personal-history.component'; describe('GeneralPersonalHistoryComponent', () => { let component: GeneralPersonalHistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let populate$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const masterData = () => ({ + typeOfTobaccoProducts: [ + { personalHabitTypeID: 1, habitValue: 'Beedi' }, + { personalHabitTypeID: 2, habitValue: 'Cigarette' }, + { personalHabitTypeID: 3, habitValue: 'Gutka' }, + ], + typeOfAlcoholProducts: [{ habitValue: 'Beer' }, { habitValue: 'Whisky' }], + quantityOfAlcoholIntake: [{ habitValue: '1-2' }], + frequencyOfAlcoholIntake: [{ habitValue: 'Daily' }], + AllergicReactionTypes: [ + { name: 'Rash', allergicReactionTypeID: 1 }, + { name: 'Other', allergicReactionTypeID: 11 }, + ], + }); + + const history = () => ({ + statusCode: 200, + data: { + PersonalHistory: { + dietaryType: 'Veg', + riskySexualPracticesStatus: '1', + tobaccoUseStatus: 'Yes', + alcoholIntakeStatus: 'Yes', + allergyStatus: 'Yes', + tobaccoList: [ + { + tobaccoUseType: 'Beedi', + numberperDay: 5, + duration: 2, + durationUnit: 'Years', + }, + { + tobaccoUseType: { personalHabitTypeID: 2 }, + numberperWeek: 3, + duration: 1, + durationUnit: 'Years', + }, + { tobaccoUseType: 'Unknown' }, + ], + alcoholList: [ + { + alcoholType: 'Beer', + alcoholIntakeFrequency: 'Daily', + avgAlcoholConsumption: '1-2', + duration: 1, + durationUnit: 'Years', + }, + { alcoholType: null, avgAlcoholConsumption: null }, + ], + allergicList: [ + { + allergyType: 'Drugs', + typeOfAllergicReactions: [{ name: 'Rash' }], + otherAllergicReaction: 'x', + snomedTerm: 'Penicillin', + snomedCode: '123', + }, + { allergyType: 'Food', typeOfAllergicReactions: [] }, + ], + }, + }, + }); + + const makeForm = () => + new FormGroup({ + dietaryType: new FormControl(null), + physicalActivityType: new FormControl(null), + riskySexualPracticesStatus: new FormControl(null), + tobaccoUseStatus: new FormControl(null), + alcoholIntakeStatus: new FormControl(null), + allergyStatus: new FormControl(null), + vanID: new FormControl(7), + parkingPlaceID: new FormControl(8), + tobaccoList: new FormArray([]), + alcoholList: new FormArray([]), + allergicList: new FormArray([]), + }); + + const arr = (name: string) => + component.generalPersonalHistoryForm.controls[name] as FormArray; + + beforeEach(async () => { + populate$ = new BehaviorSubject(null); + doctor = autoSpy(DoctorService, { + populateHistoryResponse$: populate$.asObservable(), + }); + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject({ age: '30 Years', ageVal: 30 }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MaterialModule], declarations: [GeneralPersonalHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + FormBuilder, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(GeneralPersonalHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + component.generalPersonalHistoryForm = makeForm(); + spyOn(console, 'log'); + }); + + describe('new entry mode', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('initialises language and beneficiary; waits for master data', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.beneficiary.ageVal).toBe(30); + expect(arr('tobaccoList').length).toBe(0); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('adds one row of each list when master data arrives (only once)', () => { + master$.next(masterData()); + expect(arr('tobaccoList').length).toBe(1); + expect(arr('alcoholList').length).toBe(1); + expect(arr('allergicList').length).toBe(1); + expect(component.tobaccoSelectList[0].length).toBe(3); + expect(component.alcoholSelectList[0].length).toBe(2); + expect(component.allerySelectList[0].length).toBe(3); + master$.next(masterData()); + expect(arr('tobaccoList').length).toBe(1); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('renders list sections when statuses are Yes', () => { + master$.next(masterData()); + component.generalPersonalHistoryForm.patchValue({ + tobaccoUseStatus: 'Yes', + alcoholIntakeStatus: 'Yes', + allergyStatus: 'Yes', + }); + fixture.detectChanges(); + expect(component.tobaccoUseStatus).toBe('Yes'); + expect(component.alcoholIntakeStatus).toBe('Yes'); + expect(component.allergyStatus).toBe('Yes'); + expect(component.tobaccoList.length).toBe(1); + expect( + fixture.nativeElement.querySelectorAll('mat-select').length + ).toBeGreaterThan(2); + }); + + it('list getters return null when not arrays', () => { + master$.next(masterData()); + expect(component.getTobaccoList()?.length).toBe(1); + expect(component.getAlcoholList()?.length).toBe(1); + expect(component.getAllergyList()?.length).toBe(1); + const saved = component.generalPersonalHistoryForm; + component.generalPersonalHistoryForm = new FormGroup({}); + expect(component.getTobaccoList()).toBeNull(); + expect(component.getAlcoholList()).toBeNull(); + expect(component.getAllergyList()).toBeNull(); + component.generalPersonalHistoryForm = saved; + }); + + it('populateHistoryResponse without PersonalHistory is ignored', () => { + populate$.next({ statusCode: 200, data: {} }); + populate$.next({ statusCode: 500, data: null }); + expect(component.personalHistoryData).toBeUndefined(); + }); + }); + + describe('update mode with populated history', () => { + it('patches tobacco, alcohol and allergy rows from history', () => { + component.mode = 'update'; + fixture.detectChanges(); + expect(arr('tobaccoList').length).toBe(0); + master$.next(masterData()); + populate$.next(history()); + expect(component.personalHistoryData.dietaryType).toBe('Veg'); + const t = arr('tobaccoList'); + expect(t.length).toBe(3); + expect(t.at(0).value.tobaccoUseType.habitValue).toBe('Beedi'); + expect(t.at(0).value.number).toBe(5); + expect(t.at(0).value.perDay).toBeTrue(); + expect(t.at(0).get('number')?.enabled).toBeTrue(); + expect(t.at(1).value.tobaccoUseType.habitValue).toBe('Cigarette'); + expect(t.at(1).value.number).toBe(3); + expect(t.at(1).value.perDay).toBeFalse(); + expect(t.at(2).get('number')?.disabled).toBeTrue(); + const a = arr('alcoholList'); + expect(a.length).toBe(2); + expect(a.at(0).value.typeOfAlcohol).toEqual({ habitValue: 'Beer' }); + expect(a.at(0).get('duration')?.enabled).toBeTrue(); + expect(a.at(1).dirty).toBeFalse(); + const al = arr('allergicList'); + expect(al.length).toBe(2); + expect(al.at(0).value.allergyType.allergyType).toBe('Drugs'); + expect(al.at(0).value.enableOtherAllergy).toBeTrue(); + expect(al.at(0).value.typeOfAllergicReactions).toEqual([ + { name: 'Rash', allergicReactionTypeID: 1 }, + ]); + // row 0 is computed before row 1's type is resolved, so nothing is excluded + expect(component.allerySelectList[0].length).toBe(3); + expect( + component.allerySelectList[1].map((x: any) => x.allergyType) + ).toEqual(['Food', 'Environmental']); + expect(component.previousSelectedAlleryList.length).toBe(2); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); + + it('handles history with missing lists and pre-sized arrays', () => { + component.mode = 'update'; + fixture.detectChanges(); + master$.next(masterData()); + const h: any = history(); + h.data.PersonalHistory.tobaccoList = [ + { tobaccoUseType: 'Gutka', number: 1 }, + ]; + h.data.PersonalHistory.alcoholList = []; + delete h.data.PersonalHistory.allergicList; + component.generalPersonalHistoryForm.setControl( + 'alcoholList', + new FormArray([ + component.initAlcoholList(), + component.initAlcoholList(), + ]) + ); + component.generalPersonalHistoryForm.setControl( + 'tobaccoList', + new FormArray([ + component.initTobaccoList(), + component.initTobaccoList(), + ]) + ); + populate$.next(h); + expect(arr('tobaccoList').length).toBe(1); + expect(arr('tobaccoList').at(0).get('perDay')?.value).toBeNull(); + expect(arr('tobaccoList').at(0).get('perDay')?.disabled).toBeTrue(); + expect(arr('alcoholList').length).toBe(0); + expect(arr('allergicList').length).toBe(1); + component.personalHistoryData = null; + component.handlePersonalTobaccoHistoryData(); + component.handlePersonalAlcoholHistoryData(); + component.handlePersonalAllergyHistoryData(); + expect(arr('tobaccoList').length).toBe(1); + }); + }); + + describe('view mode', () => { + it('fetches saved history after master data and converts risky status', () => { + doctor.getGeneralHistoryDetails.and.callFake(() => of(history())); + component.mode = 'view'; + fixture.detectChanges(); + master$.next(masterData()); + populate$.next(history()); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('b1', 'v1'); + expect( + component.generalPersonalHistoryForm.value.riskySexualPracticesStatus + ).toBeTrue(); + expect(arr('tobaccoList').length).toBe(3); + expect(arr('allergicList').length).toBe(2); + }); + + it('converts non-1 risky status to false and ignores empty history', () => { + const h: any = history(); + h.data.PersonalHistory.riskySexualPracticesStatus = '0'; + doctor.getGeneralHistoryDetails.and.returnValue(of(h)); + fixture.detectChanges(); + master$.next(masterData()); + component.getGeneralHistory('b1', 'v1'); + expect(component.personalHistoryData.riskySexualPracticesStatus).toBe( + false + ); + const h2: any = history(); + h2.data.PersonalHistory.riskySexualPracticesStatus = null; + doctor.getGeneralHistoryDetails.and.returnValue(of(h2)); + component.getGeneralHistory('b1', 'v1'); + expect(component.personalHistoryData.riskySexualPracticesStatus).toBe( + null + ); + component.personalHistoryData = undefined; + doctor.getGeneralHistoryDetails.and.returnValue(of(null)); + component.getGeneralHistory('b1', 'v1'); + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getGeneralHistory('b1', 'v1'); + expect(component.personalHistoryData).toBeUndefined(); + }); + }); + + describe('tobacco', () => { + let m: any[]; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + m = component.tobaccoMasterData; + }); + + it('addTobacco excludes selected types and honours avoidNullValue', () => { + arr('tobaccoList').at(0).patchValue({ tobaccoUseType: m[0] }); + component.addTobacco(); + expect( + component.tobaccoSelectList[1].map((x: any) => x.habitValue) + ).toEqual(['Cigarette', 'Gutka']); + expect(arr('tobaccoList').length).toBe(2); + component.addTobacco(true); + expect(arr('tobaccoList').length).toBe(2); + expect(component.tobaccoSelectList.length).toBe(3); + component.tobaccoMasterData = null; + component.addTobacco(); + expect(component.tobaccoSelectList.length).toBe(3); + expect(arr('tobaccoList').length).toBe(3); + }); + + it('filterTobaccoList moves selection between lists and enables number', () => { + component.addTobacco(); + const row = arr('tobaccoList').at(0); + row.patchValue({ tobaccoUseType: m[0], otherTobaccoUseType: 'x' }); + component.filterTobaccoList({ value: m[0] }, 0, row); + expect(row.value.otherTobaccoUseType).toBeNull(); + expect(row.get('number')?.enabled).toBeTrue(); + expect(component.tobaccoSelectList[1]).not.toContain(m[0]); + component.filterTobaccoList({ value: m[1] }, 0, row); + expect(component.tobaccoSelectList[1][0]).toBe(m[0]); + expect(component.previousSelectedTobaccoList[0]).toBe(m[1]); + component.filterTobaccoList({ value: m[2] }, 0); + expect(component.previousSelectedTobaccoList[0]).toBe(m[2]); + }); + + it('filterTobaccoList without selected value disables dependent fields', () => { + const row = arr('tobaccoList').at(0); + row.get('number')?.enable(); + row.get('perDay')?.enable(); + component.filterTobaccoList({ value: m[0] }, 0, row); + expect(row.get('number')?.disabled).toBeTrue(); + expect(row.get('perDay')?.disabled).toBeTrue(); + expect(row.get('durationUnit')?.disabled).toBeTrue(); + }); + + it('removeTobacco: cancel keeps row but marks form dirty', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeTobacco(0, arr('tobaccoList').at(0)); + expect(arr('tobaccoList').length).toBe(1); + expect(component.generalPersonalHistoryForm.dirty).toBeTrue(); + }); + + it('removeTobacco single row resets it', () => { + const row = arr('tobaccoList').at(0); + row.patchValue({ tobaccoUseType: m[0] }); + row.get('number')?.enable(); + row.markAsTouched(); + component.removeTobacco(0, row); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(row.value.tobaccoUseType).toBeNull(); + expect(row.get('number')?.disabled).toBeTrue(); + expect(row.touched).toBeFalse(); + }); + + it('removeTobacco from many returns value to other lists', () => { + component.addTobacco(); + component.previousSelectedTobaccoList = [m[1], m[0]]; + component.tobaccoSelectList = [[m[2]], [m[0]]]; + component.removeTobacco(1, arr('tobaccoList').at(1)); + expect(arr('tobaccoList').length).toBe(1); + expect(component.tobaccoSelectList).toEqual([[m[0], m[2]]]); + component.addTobacco(); + component.previousSelectedTobaccoList = []; + component.removeTobacco(1); + expect(arr('tobaccoList').length).toBe(1); + }); + + it('enableFields toggles perDay/duration by number', () => { + const row = arr('tobaccoList').at(0); + row.get('number')?.enable(); + row.patchValue({ number: 4 }); + component.enableFields(row); + expect(row.get('perDay')?.enabled).toBeTrue(); + expect(row.get('duration')?.enabled).toBeTrue(); + row.patchValue({ number: null }); + component.enableFields(row); + expect(row.get('perDay')?.disabled).toBeTrue(); + expect(row.get('durationUnit')?.disabled).toBeTrue(); + }); + + it('perDayChange copies number into per-day or per-week field', () => { + component.addTobacco(); + component.addTobacco(); + const t = arr('tobaccoList'); + [0, 1].forEach(i => { + t.at(i).get('number')?.enable(); + t.at(i).get('perDay')?.enable(); + }); + t.at(0).patchValue({ number: 5, perDay: true, numberperWeek: 9 }); + t.at(1).patchValue({ number: 7, perDay: false, numberperDay: 9 }); + component.perDayChange(); + expect(t.at(0).value.numberperDay).toBe(5); + expect(t.at(0).value.numberperWeek).toBeNull(); + expect(t.at(1).value.numberperWeek).toBe(7); + expect(t.at(1).value.numberperDay).toBeNull(); + expect(t.at(2).value.numberperDay).toBeNull(); + }); + + it('checkTobaccoValidity', () => { + const g: any = component.initTobaccoList(); + expect(component.checkTobaccoValidity(g)).toBeTrue(); + g.enable(); + g.patchValue({ + tobaccoUseType: m[0], + number: 1, + duration: 1, + durationUnit: 'Years', + }); + expect(component.checkTobaccoValidity(g)).toBeFalse(); + }); + + it('checkTobaccoStatus resets rows only when present', () => { + arr('tobaccoList').at(0).patchValue({ tobaccoUseType: m[0] }); + component.checkTobaccoStatus(); + expect(arr('tobaccoList').at(0).value.tobaccoUseType).toBeNull(); + arr('tobaccoList').clear(); + component.checkTobaccoStatus(); + expect(arr('tobaccoList').length).toBe(0); + }); + }); + + describe('alcohol', () => { + let m: any[]; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + m = component.alcoholMasterData; + }); + + it('addAlcohol excludes selected types and honours avoidNullValue', () => { + arr('alcoholList').at(0).patchValue({ typeOfAlcohol: m[0] }); + component.addAlcohol(); + expect(component.alcoholSelectList[1]).toEqual([m[1]]); + component.addAlcohol(true); + expect(arr('alcoholList').length).toBe(2); + component.alcoholMasterData = null; + component.addAlcohol(); + expect(component.alcoholSelectList.length).toBe(3); + expect(arr('alcoholList').length).toBe(3); + }); + + it('filterAlcoholList moves selection and enables frequency', () => { + component.addAlcohol(); + const row = arr('alcoholList').at(0); + row.patchValue({ typeOfAlcohol: m[0], otherAlcoholType: 'x' }); + component.filterAlcoholList({ value: m[0] }, 0, row); + expect(row.value.otherAlcoholType).toBeNull(); + expect(row.get('alcoholIntakeFrequency')?.enabled).toBeTrue(); + expect(component.alcoholSelectList[1]).not.toContain(m[0]); + component.filterAlcoholList({ value: m[1] }, 0, row); + expect(component.alcoholSelectList[1]).toContain(m[0]); + expect(component.previousSelectedAlcoholList[0]).toBe(m[1]); + }); + + it('filterAlcoholList without selected value disables dependent fields', () => { + const row = arr('alcoholList').at(0); + row.get('alcoholIntakeFrequency')?.enable(); + component.filterAlcoholList({ value: m[0] }, 0, row); + expect(row.get('alcoholIntakeFrequency')?.disabled).toBeTrue(); + expect(row.get('durationUnit')?.disabled).toBeTrue(); + component.filterAlcoholList({ value: m[1] }, 0); + expect(component.previousSelectedAlcoholList[0]).toBe(m[1]); + }); + + it('removeAlcohol cancel / single / many', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeAlcohol(0, arr('alcoholList').at(0)); + expect(arr('alcoholList').length).toBe(1); + confirm.confirm.and.returnValue(of(true)); + const row = arr('alcoholList').at(0); + row.patchValue({ typeOfAlcohol: m[0] }); + row.get('duration')?.enable(); + component.removeAlcohol(0, row); + expect(row.value.typeOfAlcohol).toBeNull(); + expect(row.get('duration')?.disabled).toBeTrue(); + // reset/disable of the child recomputes pristine, undoing markAsDirty + expect(component.generalPersonalHistoryForm.dirty).toBeFalse(); + + component.addAlcohol(); + component.previousSelectedAlcoholList = [m[0], m[1]]; + component.alcoholSelectList = [[m[0]], [m[1]]]; + component.removeAlcohol(1, arr('alcoholList').at(1)); + expect(arr('alcoholList').length).toBe(1); + expect(component.alcoholSelectList).toEqual([[m[0], m[1]]]); + component.addAlcohol(); + component.previousSelectedAlcoholList = []; + component.removeAlcohol(1); + expect(arr('alcoholList').length).toBe(1); + }); + + it('onChangeAlcIntakFreq and onChangeAvgAlcoholConsumption', () => { + const row = arr('alcoholList').at(0); + row.get('alcoholIntakeFrequency')?.enable(); + row.patchValue({ alcoholIntakeFrequency: 'Daily' }); + component.onChangeAlcIntakFreq(row); + expect(row.get('avgAlcoholConsumption')?.enabled).toBeTrue(); + row.patchValue({ avgAlcoholConsumption: '1-2' }); + component.onChangeAvgAlcoholConsumption(row); + expect(row.get('duration')?.enabled).toBeTrue(); + row.patchValue({ avgAlcoholConsumption: null }); + component.onChangeAvgAlcoholConsumption(row); + expect(row.get('duration')?.disabled).toBeTrue(); + row.patchValue({ alcoholIntakeFrequency: null }); + component.onChangeAlcIntakFreq(row); + expect(row.get('avgAlcoholConsumption')?.disabled).toBeTrue(); + expect(row.get('durationUnit')?.disabled).toBeTrue(); + }); + + it('checkAlcoholValidity', () => { + const g: any = component.initAlcoholList(); + expect(component.checkAlcoholValidity(g)).toBeTrue(); + g.enable(); + g.patchValue({ + typeOfAlcohol: m[0], + alcoholIntakeFrequency: 'Daily', + avgAlcoholConsumption: '1-2', + duration: 1, + durationUnit: 'Years', + }); + expect(component.checkAlcoholValidity(g)).toBeFalse(); + }); + + it('checkAlcoholStatus resets rows only when present', () => { + arr('alcoholList').at(0).patchValue({ typeOfAlcohol: m[0] }); + component.checkAlcoholStatus(); + expect(arr('alcoholList').at(0).value.typeOfAlcohol).toBeNull(); + arr('alcoholList').clear(); + component.checkAlcoholStatus(); + expect(arr('alcoholList').length).toBe(0); + }); + }); + + describe('allergy', () => { + let m: any[]; + beforeEach(() => { + fixture.detectChanges(); + master$.next(masterData()); + m = component.allergyMasterData; + }); + + it('addAllergy adds until all types used', () => { + component.selectedSnomedTerm = 'x'; + arr('allergicList').at(0).patchValue({ allergyType: m[0] }); + component.addAllergy(); + expect(component.selectedSnomedTerm).toBeNull(); + expect(component.allerySelectList[1]).toEqual([m[1], m[2]]); + component.addAllergy(true); + expect(arr('allergicList').length).toBe(2); + component.addAllergy(); + expect(arr('allergicList').length).toBe(3); + component.addAllergy(); + expect(arr('allergicList').length).toBe(3); + }); + + it('filterAlleryList moves selection and toggles fields', () => { + component.addAllergy(); + const row = arr('allergicList').at(0); + row.patchValue({ allergyType: m[0] }); + component.filterAlleryList({ value: m[0] }, 0, row); + expect(row.get('snomedTerm')?.enabled).toBeTrue(); + expect(row.get('typeOfAllergicReactions')?.enabled).toBeTrue(); + expect(component.allerySelectList[1]).not.toContain(m[0]); + row.patchValue({ allergyType: null }); + component.filterAlleryList({ value: m[1] }, 0, row); + expect(component.allerySelectList[1]).toContain(m[0]); + expect(row.get('snomedTerm')?.disabled).toBeTrue(); + expect(row.get('typeOfAllergicReactions')?.enabled).toBeTrue(); + }); + + it('removeAllergy cancel / single / many', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeAllergy(0, arr('allergicList').at(0)); + expect(component.generalPersonalHistoryForm.dirty).toBeFalse(); + confirm.confirm.and.returnValue(of(true)); + component.selectedSnomedTerm = 'x'; + const row = arr('allergicList').at(0); + row.patchValue({ allergyType: m[0] }); + component.removeAllergy(0, row); + expect(row.value.allergyType).toBeNull(); + expect(component.selectedSnomedTerm).toBeNull(); + + component.addAllergy(); + component.previousSelectedAlleryList = [m[0], m[1]]; + component.allerySelectList = [[m[0]], [m[1]]]; + component.selectedSnomedTerm = 'y'; + component.removeAllergy(1, arr('allergicList').at(1)); + expect(arr('allergicList').length).toBe(1); + expect(component.allerySelectList).toEqual([[m[0], m[1]]]); + expect(component.selectedSnomedTerm).toBeNull(); + component.addAllergy(); + component.previousSelectedAlleryList = []; + component.removeAllergy(1); + expect(arr('allergicList').length).toBe(1); + }); + + it('canEnableOtherAllergy depends on reaction type 11', () => { + const row = arr('allergicList').at(0); + row.get('typeOfAllergicReactions')?.enable(); + row.patchValue({ + typeOfAllergicReactions: [{ allergicReactionTypeID: 11 }], + }); + component.canEnableOtherAllergy(row); + expect(row.value.enableOtherAllergy).toBeTrue(); + row.patchValue({ + typeOfAllergicReactions: [{ allergicReactionTypeID: 1 }], + }); + component.canEnableOtherAllergy(row); + expect(row.value.enableOtherAllergy).toBeFalse(); + }); + + it('checkAllergyValidity', () => { + const g: any = component.initAllergyList(); + expect(component.checkAllergyValidity(g)).toBeTrue(); + g.enable(); + g.patchValue({ + allergyType: m[0], + snomedTerm: 'Pen', + snomedCode: '1', + typeOfAllergicReactions: [{}], + }); + expect(component.checkAllergyValidity(g)).toBeFalse(); + }); + + it('checkAllergicStatus resets rows only when present', () => { + arr('allergicList').at(0).patchValue({ allergyType: m[0] }); + component.checkAllergicStatus(); + expect(arr('allergicList').at(0).value.allergyType).toBeNull(); + arr('allergicList').clear(); + component.checkAllergicStatus(); + expect(arr('allergicList').length).toBe(0); + }); + + it('searchComponents opens allergen search for 3+ chars and patches result', () => { + const row = arr('allergicList').at(0); + row.get('snomedTerm')?.enable(); + row.patchValue({ snomedTerm: 'Pe' }); + component.searchComponents('Pe', 0, row); + expect(dialog.open).not.toHaveBeenCalled(); + + dialog.open.and.returnValue({ + afterClosed: () => of({ component: 'Peanut', componentNo: '999' }), + }); + row.patchValue({ snomedTerm: 'Pea' }); + component.searchComponents('Pea', 0, row); + expect(dialog.open).toHaveBeenCalledWith(AllergenSearchComponent, { + data: { searchTerm: 'Pea' }, + }); + expect(row.value.snomedTerm).toBe('Peanut'); + expect(row.value.snomedCode).toBe('999'); + expect(row.value.allergyName).toBe('Peanut'); + expect(component.selectedSnomedTerm).toBe('Peanut'); + expect(component.countForSearch).toBe(0); + expect(component.componentFlag).toBeTrue(); + expect(component.enableAlert).toBeFalse(); + + dialog.open.and.returnValue({ afterClosed: () => of(undefined) }); + component.searchComponents('Pea', 0, row); + expect(component.enableAlert).toBeTrue(); + expect(component.snomedTerm).toBeNull(); + expect(component.snomedCode).toBeNull(); + }); + + it('searchComponents ignores missing term', () => { + component.searchComponents(null, 5, arr('allergicList').at(0)); + expect(dialog.open).not.toHaveBeenCalled(); + }); + + it('removeSnomedCode clears edited snomed term', () => { + const row = arr('allergicList').at(0); + row.get('snomedTerm')?.enable(); + row.patchValue({ + snomedTerm: 'Peanut butter', + snomedCode: '1', + allergyName: 'Peanut', + }); + component.selectedSnomedTerm = undefined; + component.removeSnomedCode(row, 0); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.personalHistoryANC_OPD_NCD_PNC + .snomedTermRemoved + ); + expect(row.value.snomedCode).toBeNull(); + expect(row.value.allergyName).toBeNull(); + expect(component.selectedSnomedTerm).toBeNull(); + expect(component.countForSearch).toBe(0); + }); + + it('removeSnomedCode clears when term emptied; keeps unchanged term', () => { + const row = arr('allergicList').at(0); + row.get('snomedTerm')?.enable(); + component.selectedSnomedTerm = 'Peanut'; + component.countForSearch = 0; + row.patchValue({ snomedTerm: null, snomedCode: '1' }); + component.removeSnomedCode(row, 0); + expect(row.value.snomedCode).toBeNull(); + expect(confirm.alert).toHaveBeenCalledTimes(1); + + component.selectedSnomedTerm = 'Peanut'; + row.patchValue({ snomedTerm: ' Peanut ', snomedCode: '2' }); + component.removeSnomedCode(row, 0); + expect(row.value.snomedCode).toBe('2'); + + // later row than last searched: selection forgotten, nothing cleared + component.countForSearch = 0; + component.removeSnomedCode(row, 1); + expect(component.selectedSnomedTerm).toBeNull(); + expect(row.value.snomedCode).toBe('2'); + expect(confirm.alert).toHaveBeenCalledTimes(1); + }); + + it('sortAllergyList sorts by allergyType', () => { + const list = [ + { allergyType: 'b' }, + { allergyType: 'a' }, + { allergyType: 'a' }, + ]; + component.sortAllergyList(list); + expect(list.map(x => x.allergyType)).toEqual(['a', 'a', 'b']); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('misc', () => { + beforeEach(() => fixture.detectChanges()); + + it('sort tobacco / alcohol by habitValue', () => { + const t = [{ habitValue: 'b' }, { habitValue: 'a' }, { habitValue: 'a' }]; + component.sortTobaccoList(t); + expect(t.map(x => x.habitValue)).toEqual(['a', 'a', 'b']); + const a = [{ habitValue: 'z' }, { habitValue: 'y' }, { habitValue: 'z' }]; + component.sortAlcoholList(a); + expect(a.map(x => x.habitValue)).toEqual(['y', 'z', 'z']); + }); + + it('validateDuration alerts on duration beyond age', () => { + const g: any = component.initTobaccoList(); + g.enable(); + g.patchValue({ duration: 40, durationUnit: 'Years' }); + component.validateDuration(g); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.durationGreaterThanAge + ); + expect(g.value.duration).toBeNull(); + }); + + it('validateDuration toggles unit field', () => { + const g: any = component.initTobaccoList(); + g.get('duration').enable(); + g.patchValue({ duration: 2 }); + component.validateDuration(g); + expect(g.get('durationUnit').enabled).toBeTrue(); + g.patchValue({ durationUnit: 'Years' }); + component.validateDuration(g); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(g.value.durationUnit).toBe('Years'); + g.patchValue({ duration: null }); + component.validateDuration(g); + expect(g.get('durationUnit').disabled).toBeTrue(); + }); + + const previousCases: [string, string, () => any][] = [ + [ + 'getPreviousTobaccoHistory', + 'getPreviousTobaccoHistory', + () => LANGUAGE_EN.previousTobaccohistoryDet, + ], + [ + 'getPreviousAlcoholHistory', + 'getPreviousAlcoholHistory', + () => + LANGUAGE_EN.historyData.Alcoholhistory.previousalcoholhistorydetails, + ], + [ + 'getPreviousAllergyHistory', + 'getPreviousAllergyHistory', + () => LANGUAGE_EN.previousAllergyhistoryDet, + ], + ]; + + previousCases.forEach(([method, svc, title]) => { + it(`${method} opens dialog with data`, () => { + component.visitCategory = 'General OPD'; + const data = { data: [{}] }; + nurse[svc].and.returnValue(of({ statusCode: 200, data })); + (component as any)[method](); + expect(nurse[svc]).toHaveBeenCalledWith('b1', 'General OPD'); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { dataList: data, title: title() }, + }); + }); + + it(`${method} alerts on empty, failure and error`, () => { + nurse[svc].and.returnValue(of({ statusCode: 200, data: { data: [] } })); + (component as any)[method](); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + nurse[svc].and.returnValue(of({ statusCode: 500, data: null })); + (component as any)[method](); + nurse[svc].and.returnValue(throwingObs()); + (component as any)[method](); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + expect(confirm.alert).toHaveBeenCalledTimes(3); + }); + }); + + it('ngOnDestroy unsubscribes and resets the form', () => { + master$.next(masterData()); + component.generalPersonalHistoryForm.patchValue({ dietaryType: 'Veg' }); + const subs = [ + component.nurseMasterDataSubscription, + component.beneficiaryDetailSubscription, + ]; + subs.forEach(s => spyOn(s, 'unsubscribe').and.callThrough()); + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + expect(component.generalPersonalHistoryForm.value.dietaryType).toBeNull(); + component.nurseMasterDataSubscription = null; + component.beneficiaryDetailSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/general-opd-history/physical-activity-history/physical-activity-history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/general-opd-history/physical-activity-history/physical-activity-history.component.spec.ts index 69138765..8f0f4dd1 100644 --- a/src/app/app-modules/nurse-doctor/history/general-opd-history/physical-activity-history/physical-activity-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/general-opd-history/physical-activity-history/physical-activity-history.component.spec.ts @@ -20,27 +20,238 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services/beneficiary-details.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../../shared/services'; +import { IdrsscoreService } from '../../../shared/services/idrsscore.service'; import { PhysicalActivityHistoryComponent } from './physical-activity-history.component'; describe('PhysicalActivityHistoryComponent', () => { let component: PhysicalActivityHistoryComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let idrs: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let ben$: BehaviorSubject; + let form: FormGroup; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [PhysicalActivityHistoryComponent], - }).compileComponents(); - })); + const master = { + physicalActivity: [ + { pAID: 1, activityType: 'Vigorous', score: 0 }, + { pAID: 2, activityType: 'Sedentary', score: 30 }, + ], + }; - beforeEach(() => { + async function setup(mode = 'new', session: any = {}) { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + idrs = autoSpy(IdrsscoreService, {}, undefined); + master$ = new BehaviorSubject(null); + ben$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PhysicalActivityHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '11', visitID: '22', ...session }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { provide: IdrsscoreService, useValue: idrs }, + { + provide: MasterdataService, + useValue: { nurseMasterData$: master$.asObservable() }, + }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: ben$.asObservable() }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(PhysicalActivityHistoryComponent, '') + .compileComponents(); + confirm = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + spyOn(console, 'log'); fixture = TestBed.createComponent(PhysicalActivityHistoryComponent); component = fixture.componentInstance; + form = new FormGroup({ + activityType: new FormControl(null), + pAID: new FormControl(null), + score: new FormControl(null), + }); + component.physicalActivityHistoryForm = form; + component.mode = mode; fixture.detectChanges(); + } + + describe('new mode', () => { + beforeEach(async () => setup('new')); + + it('sets language and loads master questions', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.masterData).toBeUndefined(); + master$.next(master); + expect(component.physicalActivityQuestions).toBe(master.physicalActivity); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('stores beneficiary age or 0', () => { + ben$.next({ ageVal: 45 }); + expect(component.age).toBe(45); + ben$.next({}); + expect(component.age).toBe(0); + }); + + it('calculateIDRSScore patches form and updates IDRS service', () => { + master$.next(master); + component.calculateIDRSScore({ value: 'Sedentary' }, form); + expect(form.value.pAID).toBe(2); + expect(form.value.score).toBe(30); + expect(idrs.setIRDSscorePhysicalActivity).toHaveBeenCalledWith(30); + expect(idrs.setIDRSScoreFlag).toHaveBeenCalled(); + }); + + describe('getPreviousPhysicalActivityHistory', () => { + const err = () => LANGUAGE_EN.alerts.info.errorFetchingHistory; + it('opens dialog with data (visitType is never set)', () => { + const payload = { data: [1] }; + nurse.getPreviousPhysicalActivityHistory.and.returnValue( + of({ statusCode: 200, data: payload }) + ); + component.getPreviousPhysicalActivityHistory(); + expect(nurse.getPreviousPhysicalActivityHistory).toHaveBeenCalledWith( + '11', + undefined + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: payload, + title: LANGUAGE_EN.previousPhyscialActivityHistoryDetails, + }, + }); + }); + it('alerts when empty', () => { + nurse.getPreviousPhysicalActivityHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousPhysicalActivityHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.historyData.ancHistory.previousHistoryDetails + .pastHistoryalert + ); + }); + it('alerts on non-200', () => { + nurse.getPreviousPhysicalActivityHistory.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPreviousPhysicalActivityHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + it('alerts on failure', () => { + nurse.getPreviousPhysicalActivityHistory.and.returnValue(throwingObs()); + component.getPreviousPhysicalActivityHistory(); + expect(confirm.alert).toHaveBeenCalledWith(err(), 'error'); + }); + }); + }); + + describe('view mode, non-NCD visit', () => { + beforeEach(async () => setup('view', { visitCategory: 'General OPD' })); + + it('does not fetch history', () => { + master$.next(master); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('view mode, NCD screening', () => { + beforeEach(async () => setup('view', { visitCategory: 'NCD screening' })); + + it('patches physical activity and sets IDRS score', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + FamilyHistory: {}, + PhysicalActivityHistory: { activityType: 'Sedentary', pAID: 2 }, + }, + }) + ); + master$.next(master); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith('11', '22'); + expect(form.value.activityType).toBe('Sedentary'); + expect(idrs.setIRDSscorePhysicalActivity).toHaveBeenCalledWith(30); + }); + + it('does not set score for unknown activity', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { + FamilyHistory: {}, + PhysicalActivityHistory: { activityType: 'Unknown' }, + }, + }) + ); + master$.next(master); + expect(form.value.activityType).toBe('Unknown'); + expect(idrs.setIRDSscorePhysicalActivity).not.toHaveBeenCalled(); + }); + + it('skips when PhysicalActivityHistory is missing', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ statusCode: 200, data: { FamilyHistory: {} } }) + ); + master$.next(master); + expect(component.physicalActivityHistoryData).toBeUndefined(); + expect(form.value.activityType).toBeNull(); + }); + + it('requires FamilyHistory to be present (current behaviour)', () => { + doctor.getGeneralHistoryDetails.and.returnValue( + of({ + statusCode: 200, + data: { PhysicalActivityHistory: { activityType: 'Sedentary' } }, + }) + ); + master$.next(master); + expect(form.value.activityType).toBeNull(); + }); + }); + + describe('view mode without visit id', () => { + beforeEach(async () => + setup('view', { visitCategory: 'NCD screening', visitID: null }) + ); + + it('does not fetch history', () => { + master$.next(master); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/history/history.component.spec.ts b/src/app/app-modules/nurse-doctor/history/history.component.spec.ts index 07f3eab1..1b352b9c 100644 --- a/src/app/app-modules/nurse-doctor/history/history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/history/history.component.spec.ts @@ -20,27 +20,107 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DoctorService } from '../shared/services/doctor.service'; import { HistoryComponent } from './history.component'; describe('HistoryComponent', () => { let component: HistoryComponent; let fixture: ComponentFixture; + let doctor: any; + let route: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup( + attendant: string, + response: any = { statusCode: 200, data: {} } + ) { + doctor = autoSpy(DoctorService); + doctor.getGeneralHistoryDetails.and.returnValue(of(response)); + doctor.setCapturedHistoryByNurse.and.returnValue(undefined); + route = { snapshot: { params: { attendant } } }; + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [HistoryComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: '100', beneficiaryRegID: '200' }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: ActivatedRoute, useValue: route }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(HistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.patientHistoryDataForm = new FormGroup({}); + component.visitCategory = 'General OPD'; + component.ngOnInit(); + } + + describe('as nurse', () => { + beforeEach(async () => setup('nurse')); + + it('clears captured history and does not fetch it', () => { + expect(component.attendant).toBe('nurse'); + expect(doctor.setCapturedHistoryByNurse).toHaveBeenCalledWith(null); + expect(doctor.getGeneralHistoryDetails).not.toHaveBeenCalled(); + expect(component.showGeneralOPD).toBeTrue(); + }); + + [ + 'General OPD', + 'ANC', + 'NCD care', + 'PNC', + 'COVID-19 Screening', + 'NCD screening', + ].forEach(cat => + it(`ngOnChanges shows general OPD tabs for ${cat}`, () => { + component.visitCategory = cat; + component.ngOnChanges(); + expect(component.showGeneralOPD).toBeTrue(); + expect(component.showCancer).toBeFalse(); + }) + ); + + ['Cancer Screening', '', undefined as any].forEach(cat => + it(`shows cancer tabs for "${cat}"`, () => { + component.visitCategory = cat; + component.showHistoryTabs(); + expect(component.showCancer).toBeTrue(); + expect(component.showGeneralOPD).toBeFalse(); + }) + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('as doctor', () => { + it('fetches history with session ids and stores it on 200', async () => { + const resp = { statusCode: 200, data: { x: 1 } }; + await setup('doctor', resp); + const session = TestBed.inject(SessionStorageService) as any; + expect(session.getItem).toHaveBeenCalledWith('visitID'); + expect(doctor.getGeneralHistoryDetails).toHaveBeenCalledWith( + '200', + '100' + ); + expect(doctor.setCapturedHistoryByNurse).toHaveBeenCalledWith(resp); + }); + + it('does not store history on non-200', async () => { + await setup('doctor', { statusCode: 5000 }); + expect(doctor.setCapturedHistoryByNurse.calls.allArgs()).toEqual([ + [null], + ]); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/idrs/idrs.component.spec.ts b/src/app/app-modules/nurse-doctor/idrs/idrs.component.spec.ts index 9c764df8..34d1bc6c 100644 --- a/src/app/app-modules/nurse-doctor/idrs/idrs.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/idrs/idrs.component.spec.ts @@ -19,28 +19,1098 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; import { IdrsComponent } from './idrs.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../shared/services'; +import { IdrsscoreService } from '../shared/services/idrsscore.service'; +import { + BeneficiaryDetailsService, + ConfirmationService, +} from '../../core/services'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { PreviousDetailsComponent } from '../../core/components/previous-details/previous-details.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; + +const QUESTIONS = [ + { idrsQuestionID: 1, question: 'd1', DiseaseQuestionType: 'Diabetes' }, + { idrsQuestionID: 2, question: 'd2', DiseaseQuestionType: 'Diabetes' }, + { idrsQuestionID: 3, question: 'a1', DiseaseQuestionType: 'Asthma' }, + { idrsQuestionID: 4, question: 'e1', DiseaseQuestionType: 'Epilepsy' }, + { + idrsQuestionID: 5, + question: 'v1', + DiseaseQuestionType: 'Vision Screening', + }, + { + idrsQuestionID: 6, + question: 't1', + DiseaseQuestionType: 'Tuberculosis Screening', + }, + { + idrsQuestionID: 7, + question: 'm1', + DiseaseQuestionType: 'Malaria Screening', + }, + { idrsQuestionID: 8, question: 'h1', DiseaseQuestionType: 'Hypertension' }, +]; describe('IdrsComponent', () => { let component: IdrsComponent; let fixture: ComponentFixture; + let idrsForm: FormGroup; + let medicalForm: FormGroup; + let idrsService: IdrsscoreService; + let nurse: any; + let doctor: any; + let confirmation: any; + let session: any; + let dialog: any; + let masterData$: BehaviorSubject; + let beneficiary$: BehaviorSubject; + let route: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + spyOn(console, 'log'); + masterData$ = new BehaviorSubject(null); + beneficiary$ = new BehaviorSubject(null); + route = { snapshot: { params: { attendant: 'nurse' } } }; + + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [IdrsComponent], - }).compileComponents(); - })); + providers: [ + ...commonTestProviders(), + FormBuilder, + IdrsscoreService, + { provide: ActivatedRoute, useValue: route }, + { provide: NurseService, useValue: autoSpy(NurseService) }, + { provide: DoctorService, useValue: autoSpy(DoctorService) }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: masterData$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: beneficiary$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(IdrsComponent, '') + .compileComponents(); + + idrsService = TestBed.inject(IdrsscoreService); + nurse = TestBed.inject(NurseService) as any; + doctor = TestBed.inject(DoctorService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + session = TestBed.inject(SessionStorageService) as any; + dialog = TestBed.inject(MatDialog) as any; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); - beforeEach(() => { + idrsForm = new FormGroup({ + idrsScore: new FormControl(null), + requiredList: new FormControl(null), + questionArray: new FormControl(null), + suspectArray: new FormControl(null), + confirmArray: new FormControl(null), + isDiabetic: new FormControl(null), + }); + medicalForm = new FormGroup({ idrsScreeningForm: idrsForm }); + }); + + function create(mode = 'new') { fixture = TestBed.createComponent(IdrsComponent); component = fixture.componentInstance; + component.idrsScreeningForm = idrsForm; + component.patientMedicalForm = medicalForm; + component.ncdScreeningMode = mode; + component.visitCategory = 'NCD screening'; fixture.detectChanges(); + return component; + } + + function diseaseOf(name: string) { + return component.diseases.find((d: any) => d.disease === name); + } + + function answer(id: number, value: any) { + component.questions1.find((q: any) => q.idrsQuestionID === id).answer = + value; + } + + describe('initialisation', () => { + it('sets language and resets the idrs service state', () => { + spyOn(idrsService, 'clearScoreFlag').and.callThrough(); + spyOn(idrsService, 'clearDiabetesSelected').and.callThrough(); + spyOn(idrsService, 'clearDiseaseSelected').and.callThrough(); + spyOn(idrsService, 'clearUnchecked').and.callThrough(); + create(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(idrsService.clearScoreFlag).toHaveBeenCalled(); + expect(idrsService.clearDiabetesSelected).toHaveBeenCalled(); + expect(idrsService.clearDiseaseSelected).toHaveBeenCalled(); + expect(idrsService.clearUnchecked).toHaveBeenCalled(); + expect(idrsForm.value.idrsScore).toBe(0); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('adds vitals/history scores into the idrs score', () => { + create(); + idrsService.setIDRSScoreWaist(10); + idrsService.setIDRSFamilyScore(20); + idrsService.setIRDSscorePhysicalActivity(30); + expect(component.idrsScoreWaist).toBe(10); + expect(component.idrsScoreFamily).toBe(20); + expect(component.IRDSscorePhysicalActivity).toBe(30); + expect(idrsForm.value.idrsScore).toBe(60); + idrsService.setIDRSScoreWaist(undefined); + idrsService.setIDRSFamilyScore(undefined); + idrsService.setIRDSscorePhysicalActivity(undefined); + expect(component.idrsScoreWaist).toBe(0); + expect(component.idrsScoreFamily).toBe(0); + expect(component.IRDSscorePhysicalActivity).toBe(0); + }); + }); + + describe('getBeneficiaryDetails', () => { + [ + { age: 25, score: 0 }, + { age: 40, score: 20 }, + { age: 60, score: 30 }, + ].forEach(({ age, score }) => { + it(`gives an age score of ${score} for age ${age}`, () => { + beneficiary$.next({ ageVal: age, beneficiaryRegID: 7 }); + create(); + expect(component.age).toBe(age); + expect(component.idrsScore).toBe(score); + expect(nurse.getNcdScreeningVisitCount).toHaveBeenCalledWith(7); + }); + }); + + it('defaults age to 0 when ageVal is missing', () => { + beneficiary$.next({ beneficiaryRegID: 7 }); + create(); + expect(component.age).toBe(0); + expect(component.idrsScore).toBe(0); + }); + + it('does not call visit count without beneficiary', () => { + create(); + expect(nurse.getNcdScreeningVisitCount).not.toHaveBeenCalled(); + }); + }); + + describe('getNurseMasterData', () => { + it('builds questions and distinct diseases and required list for adults', () => { + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + beneficiary$.next({ ageVal: 40, beneficiaryRegID: 7 }); + create(); + masterData$.next({ IDRSQuestions: QUESTIONS }); + expect(component.questions1.length).toBe(8); + expect(component.questions1[0]).toEqual({ + id: null, + idrsQuestionID: 1, + question: 'd1', + diseaseQuestionType: 'Diabetes', + answer: null, + }); + expect(component.diseases.map((d: any) => d.disease)).toEqual([ + 'Diabetes', + 'Asthma', + 'Epilepsy', + 'Vision Screening', + 'Tuberculosis Screening', + 'Malaria Screening', + 'Hypertension', + ]); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + expect(diseaseOf('Asthma').flag).toBeTrue(); + expect(idrsForm.value.requiredList).not.toContain('Diabetes'); + expect(idrsForm.value.requiredList).toContain('Asthma'); + expect(nurse.getPreviousVisitData).toHaveBeenCalled(); + }); + + it('keeps Diabetes required when the score is at least 60', () => { + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + beneficiary$.next({ ageVal: 60, beneficiaryRegID: 7 }); + create(); + component.idrsScoreWaist = 30; + masterData$.next({ IDRSQuestions: QUESTIONS }); + expect(idrsForm.value.requiredList).toContain('Diabetes'); + }); + + it('removes Diabetes from a pre-filled required list when the score is low', () => { + create(); + component.required = ['Diabetes', 'Asthma']; + component.age = 10; + masterData$.next({ IDRSQuestions: QUESTIONS }); + expect(idrsForm.value.requiredList).toEqual(['Asthma']); + }); + + it('skips previous visit for doctor attendant and loads details in view mode / specialist', () => { + route.snapshot.params.attendant = 'doctor'; + session.setItem('visitID', 1); + session.setItem('beneficiaryRegID', 2); + session.setItem('specialistFlag', '100'); + doctor.getIDRSDetails.and.returnValue(of(null)); + create('view'); + masterData$.next({ IDRSQuestions: QUESTIONS }); + expect(nurse.getPreviousVisitData).not.toHaveBeenCalled(); + expect(doctor.getIDRSDetails).toHaveBeenCalledTimes(2); + expect(doctor.getIDRSDetails).toHaveBeenCalledWith(2, 1); + }); + + it('ignores view mode without session ids and handles empty questions', () => { + create('view'); + masterData$.next({ IDRSQuestions: [] }); + expect(component.questions1).toEqual([]); + expect(doctor.getIDRSDetails).not.toHaveBeenCalled(); + }); + + it('does nothing for a null master data emission', () => { + create(); + expect(component.questions).toEqual([]); + }); + }); + + describe('getPreviousVisit', () => { + function loadMaster(age: number) { + create(); + component.age = age; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + masterData$.next({ IDRSQuestions: QUESTIONS }); + } + + it('disables chronic confirmed diseases on revisit', () => { + loadMaster(45); + nurse.getPreviousVisitData.and.returnValue( + of({ + statusCode: 200, + data: { + isDiabetic: true, + isDefectiveVision: true, + isEpilepsy: true, + questionariesData: [{ q: 1 }], + confirmedDisease: + 'Tuberculosis Screening,Malaria Screening,Asthma,Other', + }, + }) + ); + component.getPreviousVisit(); + expect(component.isDiabetic).toBeTrue(); + expect(diseaseOf('Diabetes').disabled).toBeTrue(); + expect(diseaseOf('Vision Screening').disabled).toBeTrue(); + expect(diseaseOf('Epilepsy').disabled).toBeTrue(); + expect(diseaseOf('Tuberculosis Screening').disabled).toBeTrue(); + expect(diseaseOf('Malaria Screening').disabled).toBeTrue(); + expect(diseaseOf('Asthma').disabled).toBeTrue(); + expect(component.chronicDisabled).toBeTrue(); + expect(component.confirmDiseaseArray).toEqual([ + 'Tuberculosis Screening', + 'Malaria Screening', + 'Asthma', + 'Other', + 'Diabetes', + 'Epilepsy', + 'Vision Screening', + ]); + expect(component.required).toEqual(['Hypertension']); + }); + + it('handles non-diabetic revisit without confirmed diseases', () => { + loadMaster(45); + component.confirmDiseaseArray = ['X']; + nurse.getPreviousVisitData.and.returnValue( + of({ + statusCode: 200, + data: { + isDiabetic: false, + isDefectiveVision: false, + isEpilepsy: false, + questionariesData: [{ q: 1 }], + confirmedDisease: null, + }, + }) + ); + component.getPreviousVisit(); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + expect(diseaseOf('Asthma').disabled).toBeFalse(); + expect(component.confirmDiseaseArray).toEqual(['X']); + }); + + it('skips chronic processing for young beneficiaries or no questionnaire', () => { + loadMaster(20); + nurse.getPreviousVisitData.and.returnValue( + of({ + statusCode: 200, + data: { isDiabetic: true, questionariesData: [{ q: 1 }] }, + }) + ); + component.getPreviousVisit(); + expect(component.chronicDisabled).toBeFalse(); + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: { questionariesData: null } }) + ); + component.getPreviousVisit(); + expect(component.chronicDisabled).toBeFalse(); + }); + + it('alerts on error', () => { + create(); + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + nurse.getPreviousVisitData.and.returnValue(throwingObs('bad')); + component.getPreviousVisit(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(emitted).toContain(false); + }); + + it('patches form arrays for a doctor on revisit', () => { + create(); + route.snapshot.params.attendant = 'tcspecialist'; + component.revisit = true; + component.questions1 = [ + { idrsQuestionID: 1, answer: 'yes' }, + { idrsQuestionID: 2, answer: null }, + ]; + component.suspect = ['Asthma']; + component.confirmDiseaseArray = ['Epilepsy']; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getPreviousVisit(); + expect(idrsForm.value.questionArray).toEqual([ + { idrsQuestionID: 1, answer: 'yes' }, + ]); + expect(idrsForm.value.suspectArray).toEqual(['Asthma']); + expect(idrsForm.value.confirmArray).toEqual(['Epilepsy']); + }); + }); + + describe('idrsFlagScore', () => { + let emitted: any[]; + beforeEach(() => { + create(); + component.age = 45; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + masterData$.next({ IDRSQuestions: QUESTIONS }); + emitted = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + }); + + it('emits true when the flag is cleared', () => { + idrsService.clearScoreFlag(); + expect(emitted).toEqual([true]); + }); + + it('requires Diabetes when score >= 60 and questions unanswered', () => { + component.idrsScore = 30; + component.idrsScoreWaist = 30; + idrsService.setIDRSScoreFlag(); + expect(emitted).toEqual([false]); + expect(diseaseOf('Diabetes').flag).toBeTrue(); + expect(component.required).toContain('Diabetes'); + }); + + it('does not require Diabetes when all its questions are answered', () => { + component.idrsScore = 60; + answer(1, 'no'); + answer(2, 'no'); + idrsService.setIDRSScoreFlag(); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + expect(component.required).not.toContain('Diabetes'); + }); + + it('does not require Diabetes when already diabetic (not revisit)', () => { + component.idrsScore = 60; + component.isDiabetic = true; + idrsService.setIDRSScoreFlag(); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + expect(component.required).toContain('Asthma'); + }); + + it('uses revisit/diabetic combination to decide', () => { + component.idrsScore = 60; + component.revisit = true; + component.isDiabetic = true; + idrsService.setIDRSScoreFlag(); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + component.isDiabetic = false; + idrsService.setIDRSScoreFlag(); + expect(diseaseOf('Diabetes').flag).toBeTrue(); + }); + + it('drops Diabetes when score < 60', () => { + diseaseOf('Diabetes').flag = true; + idrsService.setIDRSScoreFlag(); + expect(diseaseOf('Diabetes').flag).toBeFalse(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('blood pressure driven hypertension suspicion', () => { + let emitted: any[]; + beforeEach(() => { + create(); + emitted = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + }); + + it('adds and removes hypertension based on systolic value', () => { + idrsService.setSystolicBp(150); + expect(component.suspect).toEqual(['Hypertension']); + idrsService.setSystolicBp(160); + expect(component.suspect).toEqual(['Hypertension']); + idrsService.setSystolicBp(120); + expect(component.suspect).toEqual([]); + }); + + it('adds and removes hypertension based on diastolic value', () => { + idrsService.setDiastolicBp(95); + expect(component.suspect).toEqual(['Hypertension']); + idrsService.setDiastolicBp(99); + expect(component.suspect).toEqual(['Hypertension']); + idrsService.setDiastolicBp(70); + expect(component.suspect).toEqual([]); + }); + + it('keeps hypertension while the other reading is high', () => { + idrsService.setSystolicBp(150); + idrsService.setDiastolicBp(70); + expect(component.suspect).toEqual(['Hypertension']); + idrsService.setDiastolicBp(95); + idrsService.setSystolicBp(120); + expect(component.suspect).toEqual(['Hypertension']); + }); + + it('ignores readings once hypertension is selected', () => { + idrsService.setHypertensionSelected(); + expect(component.hypertensionChecked).toBeTrue(); + idrsService.setSystolicBp(180); + expect(component.suspect).toEqual([]); + }); + + it('ignores undefined readings', () => { + idrsService.setSystolicBp(undefined); + idrsService.setDiastolicBp(undefined); + // initial null emission is recorded; undefined ones are skipped + expect(component.systolicValueFromVital).toBeNull(); + expect(component.diastolicValueFromVital).toBeNull(); + }); + + it('emits change when hypertension is added alongside other suspects', () => { + component.suspect = ['Asthma']; + component.addToSuspectedHyper('Hypertension'); + expect(emitted).toEqual([false]); + expect(component.systolicChange).toBeTrue(); + component.suspect = []; + component.addToSuspectedHyper('Hypertension'); + expect(emitted).toEqual([false, false]); + }); + + it('does not add when already suspected or confirmed', () => { + component.confirmDiseaseArray = ['Hypertension']; + component.addToSuspectedHyper('Hypertension'); + expect(component.suspect).toEqual([]); + component.addToSuspectedHyper('Other'); + expect(component.suspect).toEqual(['Other']); + }); + }); + + describe('removeSuspectedHyper', () => { + beforeEach(() => create()); + + it('keeps suspicion when a question answered yes', () => { + component.questions1 = [ + { diseaseQuestionType: 'Hypertension', answer: 'yes' }, + ]; + component.suspect = ['Hypertension']; + component.removeSuspectedHyper('Hypertension'); + expect(component.suspect).toEqual(['Hypertension']); + }); + + it('removes and emits when other suspects exist', () => { + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + spyOn(idrsService, 'setSuspectedArrayValue').and.callThrough(); + spyOn(idrsService, 'setDiabetesSelected').and.callThrough(); + component.suspect = ['Hypertension', 'Diabetes', 'Asthma']; + component.removeSuspectedHyper('Hypertension'); + expect(component.suspect).toEqual(['Diabetes', 'Asthma']); + expect(emitted).toEqual([false]); + expect(idrsService.setDiabetesSelected).toHaveBeenCalled(); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalled(); + }); + + it('clears flag when only non-chronic suspects remain', () => { + spyOn(idrsService, 'clearSuspectedArrayFlag').and.callThrough(); + component.systolicChange = true; + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + component.suspect = ['Hypertension']; + component.removeSuspectedHyper('Hypertension'); + expect(emitted).toEqual([false]); + expect(component.suspect).toEqual([]); + component.suspect = ['Other']; + component.removeSuspectedHyper('Hypertension'); + expect(idrsService.clearSuspectedArrayFlag).toHaveBeenCalledTimes(2); + }); + + it('removes a non-hypertension value without emitting', () => { + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + component.suspect = ['Other', 'Asthma']; + component.removeSuspectedHyper('Other'); + expect(component.suspect).toEqual(['Asthma']); + expect(emitted).toEqual([]); + }); + }); + + describe('removeSuspected / removeSuspect', () => { + beforeEach(() => create()); + + it('removeSuspected keeps value when question answered yes', () => { + component.questions1 = [{ diseaseQuestionType: 'Asthma', answer: 'yes' }]; + component.suspect = ['Asthma']; + component.removeSuspected('Asthma'); + expect(component.suspect).toEqual(['Asthma']); + }); + + it('removeSuspected handles all flag branches', () => { + spyOn(idrsService, 'clearSuspectedArrayFlag').and.callThrough(); + spyOn(idrsService, 'setSuspectedArrayValue').and.callThrough(); + spyOn(idrsService, 'setDiabetesSelected').and.callThrough(); + component.suspect = ['Asthma', 'Diabetes', 'Epilepsy']; + component.removeSuspected('Asthma'); + expect(component.suspect).toEqual(['Diabetes', 'Epilepsy']); + expect(idrsService.setDiabetesSelected).toHaveBeenCalled(); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalled(); + component.removeSuspected('Epilepsy'); + expect(idrsService.clearSuspectedArrayFlag).toHaveBeenCalledTimes(1); + component.removeSuspected('Diabetes'); + expect(component.suspect).toEqual([]); + expect(idrsService.clearSuspectedArrayFlag).toHaveBeenCalledTimes(2); + }); + + it('removeSuspect only acts when questions exist', () => { + component.questions1 = []; + component.suspect = ['Asthma']; + component.removeSuspect('Asthma'); + expect(component.suspect).toEqual(['Asthma']); + spyOn(idrsService, 'setSuspectedArrayValue').and.callThrough(); + spyOn(idrsService, 'clearSuspectedArrayFlag').and.callThrough(); + component.questions1 = [{}]; + component.suspect = ['Asthma', 'Epilepsy', 'Other']; + component.removeSuspect('Asthma'); + expect(component.suspect).toEqual(['Epilepsy', 'Other']); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalled(); + component.removeSuspect('Epilepsy'); + expect(idrsService.clearSuspectedArrayFlag).toHaveBeenCalledTimes(1); + component.removeSuspect('Other'); + expect(component.suspect).toEqual([]); + expect(idrsService.clearSuspectedArrayFlag).toHaveBeenCalledTimes(2); + }); + }); + + describe('addToSuspected / confirm array', () => { + beforeEach(() => create()); + + it('adds Diabetes and sets service flags', () => { + spyOn(idrsService, 'setDiabetesSelected').and.callThrough(); + spyOn(idrsService, 'setSuspectedArrayValue').and.callThrough(); + component.addToSuspected('Diabetes'); + component.addToSuspected('Diabetes'); + expect(component.suspect).toEqual(['Diabetes']); + expect(idrsService.setDiabetesSelected).toHaveBeenCalledTimes(1); + component.addToSuspected('Vision Screening'); + component.addToSuspected('Asthma'); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalledTimes(2); + }); + + it('does not suspect a confirmed disease', () => { + component.confirmDiseaseArray = ['Asthma']; + component.addToSuspected('Asthma'); + expect(component.suspect).toEqual([]); + }); + + it('addToconfirmDiseaseArray adds once and patches the form', () => { + component.addToconfirmDiseaseArray('Asthma'); + component.addToconfirmDiseaseArray('Asthma'); + expect(component.confirmDiseaseArray).toEqual(['Asthma']); + expect(idrsForm.value.confirmArray).toEqual(['Asthma']); + }); + + it('removeConfirmDiseaseArray removes and trims Diabetes when score low', () => { + component.confirmDiseaseArray = ['Asthma', 'Epilepsy']; + component.required = ['Diabetes', 'Asthma']; + component.removeConfirmDiseaseArray('Asthma'); + expect(component.confirmDiseaseArray).toEqual(['Epilepsy']); + expect(idrsForm.value.requiredList).toEqual(['Asthma']); + component.idrsScore = 60; + component.required = ['Diabetes']; + component.removeConfirmDiseaseArray('X'); + expect(idrsForm.value.requiredList).toEqual(['Diabetes']); + }); + + it('checkQuestionsToAddInSuspect adds Diabetes on a yes answer', () => { + component.diseases = [{ disease: 'Diabetes', confirmed: true }]; + component.questions1 = [ + { diseaseQuestionType: 'Diabetes', answer: 'no' }, + { diseaseQuestionType: 'Diabetes', answer: 'yes' }, + ]; + component.checkQuestionsToAddInSuspect('Diabetes'); + expect(component.suspect).toEqual(['Diabetes']); + expect(component.diseases[0].confirmed).toBeFalse(); + component.suspect = []; + component.checkQuestionsToAddInSuspect('Asthma'); + expect(component.suspect).toEqual([]); + }); + + it('updateDiabetesQuestionValue keeps Diabetes when score is high', () => { + component.idrsScore = 60; + component.required = ['Diabetes']; + component.updateDiabetesQuestionValue(); + expect(idrsForm.value.requiredList).toEqual(['Diabetes']); + }); + }); + + describe('uncheckedDiseases observable', () => { + beforeEach(() => { + create(); + component.diseases = [ + { disease: 'Diabetes', flag: true, confirmed: true }, + { disease: 'Asthma', flag: true, confirmed: false }, + ]; + }); + + it('re-suspects disease that has a yes answer', () => { + spyOn(idrsService, 'setDiabetesSelected').and.callThrough(); + component.questions1 = [ + { diseaseQuestionType: 'Diabetes', answer: 'yes' }, + ]; + component.confirmDiseaseArray = ['Diabetes']; + idrsService.setUnchecked('Diabetes'); + expect(component.suspect).toEqual(['Diabetes']); + expect(component.confirmDiseaseArray).toEqual([]); + expect(component.diseases[0].confirmed).toBeFalse(); + expect(idrsService.setDiabetesSelected).toHaveBeenCalled(); + expect(idrsForm.value.requiredList).toEqual(['Diabetes', 'Asthma']); + }); + + it('un-confirms disease without a yes answer', () => { + component.questions1 = [ + { diseaseQuestionType: 'Diabetes', answer: 'no' }, + ]; + idrsService.setUnchecked('Diabetes'); + expect(component.suspect).toEqual([]); + expect(component.diseases[0].confirmed).toBeFalse(); + }); + + it('re-suspects non-diabetes disease without setting diabetes flag', () => { + spyOn(idrsService, 'setDiabetesSelected').and.callThrough(); + component.questions1 = [{ diseaseQuestionType: 'Asthma', answer: 'yes' }]; + idrsService.setUnchecked('Asthma'); + expect(component.suspect).toEqual(['Asthma']); + expect(idrsService.setDiabetesSelected).not.toHaveBeenCalled(); + }); + }); + + describe('visitDiseases observable', () => { + beforeEach(() => { + create(); + component.questions1 = [{}]; + component.diseases = [ + { disease: 'Diabetes', flag: true, confirmed: false }, + { disease: 'Asthma', flag: true, confirmed: false }, + ]; + }); + + it('confirms diseases and removes them from suspects', () => { + spyOn(idrsService, 'clearDiabetesSelected').and.callThrough(); + component.suspect = ['Diabetes', 'Asthma']; + idrsService.setDiseasesSelected(['Diabetes']); + expect(component.diseases[0].confirmed).toBeTrue(); + expect(component.required).toEqual(['Asthma']); + expect(component.confirmDiseaseArray).toEqual(['Diabetes']); + expect(component.suspect).toEqual(['Asthma']); + expect(idrsService.clearDiabetesSelected).toHaveBeenCalled(); + }); + + it('ignores empty lists', () => { + idrsService.setDiseasesSelected([]); + expect(component.confirmDiseaseArray).toEqual([]); + }); + + it('deletes both confirmed diseases', () => { + component.confirmDiseaseArray = ['Hypertension', 'Diabetes']; + idrsService.setDiseasesSelected([ + 'Hypertension', + 'Diabetes', + 'deleteHypertension', + ]); + // current behaviour: indexOf of string literals => last elements spliced + expect(component.confirmDiseaseArray).toEqual([]); + }); + + it('deletes only hypertension', () => { + component.confirmDiseaseArray = ['Hypertension', 'Asthma']; + component.confirmed = ['Hypertension', 'deleteHypertension']; + component.deleteDiagnosisConfirmedDisease(); + expect(component.confirmDiseaseArray).toEqual(['Hypertension']); + expect(component.confirmed).toEqual([]); + }); + + it('deletes only diabetes', () => { + component.confirmDiseaseArray = ['Diabetes']; + component.confirmed = ['Diabetes', 'deleteDiabetes', 'x']; + component.deleteDiagnosisConfirmedDisease(); + expect(component.confirmDiseaseArray).toEqual([]); + expect(component.confirmed).toEqual(['Diabetes']); + }); + + it('does nothing when nothing matches', () => { + component.confirmDiseaseArray = ['Asthma']; + component.confirmed = ['deleteDiabetes']; + component.deleteDiagnosisConfirmedDisease(); + expect(component.confirmDiseaseArray).toEqual(['Asthma']); + }); + }); + + describe('radioChange', () => { + let emitted: any[]; + beforeEach(() => { + create(); + component.age = 45; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + masterData$.next({ IDRSQuestions: QUESTIONS }); + emitted = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + }); + + it('records a yes answer and suspects the disease', () => { + component.radioChange(component.questions1[2], 'yes', 'Asthma'); + expect(emitted).toEqual([false]); + expect(component.questions1[2].answer).toBe('yes'); + expect(component.suspect).toEqual(['Asthma']); + expect(diseaseOf('Asthma').flag).toBeFalse(); + expect(idrsForm.value.questionArray.length).toBe(1); + expect(idrsForm.value.suspectArray).toEqual(['Asthma']); + expect(idrsForm.value.requiredList).not.toContain('Asthma'); + expect(idrsForm.value.isDiabetic).toBeFalse(); + }); + + it('marks isDiabetic and keeps Diabetes required until all answered', () => { + component.idrsScore = 60; + diseaseOf('Diabetes').flag = true; + component.radioChange(component.questions1[0], 'yes', 'Diabetes'); + expect(diseaseOf('Diabetes').flag).toBeTrue(); + expect(idrsForm.value.isDiabetic).toBeTrue(); + expect(idrsForm.value.requiredList).toContain('Diabetes'); + }); + + it('trims Diabetes from required when score is low', () => { + diseaseOf('Diabetes').flag = true; + component.radioChange(component.questions1[2], 'no', 'Asthma'); + expect(idrsForm.value.requiredList).not.toContain('Diabetes'); + }); + + it('removes suspicion on a no answer', () => { + component.suspect = ['Asthma']; + component.radioChange(component.questions1[2], 'no', 'Asthma'); + expect(component.suspect).toEqual([]); + }); + + it('uses the rev list when chronic answers exist', () => { + component.chronicDisabled = true; + component.rev = [{ idrsQuestionID: 3 }, { idrsQuestionID: 99 }]; + component.radioChange(component.questions1[2], 'yes', 'Asthma'); + expect( + idrsForm.value.questionArray.map((q: any) => q.idrsQuestionID) + ).toEqual([99, 3]); + }); + + it('uses the arr list for a doctor without revisit', () => { + route.snapshot.params.attendant = 'doctor'; + component.chronicDisabled = true; + component.rev = [{ idrsQuestionID: 99 }]; + component.radioChange(component.questions1[2], 'yes', 'Asthma'); + expect( + idrsForm.value.questionArray.map((q: any) => q.idrsQuestionID) + ).toEqual([3]); + }); + }); + + describe('ngOnChanges and updateIDRSDetails', () => { + it('loads nurse details in view mode', () => { + create(); + session.setItem('visitID', 1); + session.setItem('beneficiaryRegID', 2); + doctor.getIDRSDetails.and.returnValue(of(null)); + component.ncdScreeningMode = 'view'; + component.ngOnChanges(); + expect(component.doctorScreen).toBeTrue(); + expect(doctor.getIDRSDetails).toHaveBeenCalledWith(2, 1); + }); + + it('skips fetch without session ids in view / specialist mode', () => { + create(); + session.setItem('specialistFlag', '100'); + component.ncdScreeningMode = 'view'; + component.ngOnChanges(); + expect(component.doctorScreen).toBeTrue(); + expect(doctor.getIDRSDetails).not.toHaveBeenCalled(); + }); + + it('loads nurse details for specialist flag', () => { + create(); + session.setItem('specialistFlag', '100'); + session.setItem('visitID', 1); + session.setItem('beneficiaryRegID', 2); + doctor.getIDRSDetails.and.returnValue(of(null)); + component.ngOnChanges(); + expect(doctor.getIDRSDetails).toHaveBeenCalledTimes(1); + }); + + it('does nothing in new mode', () => { + create(); + component.ngOnChanges(); + expect(component.doctorScreen).toBeFalse(); + expect(doctor.updateIDRSDetails).not.toHaveBeenCalled(); + }); + + it('updates details on success', () => { + create(); + session.setItem('visitCategory', 'NCD screening'); + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + doctor.updateIDRSDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + idrsForm.markAsDirty(); + component.ncdScreeningMode = 'update'; + component.ngOnChanges(); + expect(doctor.updateIDRSDetails).toHaveBeenCalledWith( + idrsForm, + 'NCD screening' + ); + expect(confirmation.alert).toHaveBeenCalledWith('saved', 'success'); + expect(emitted).toEqual(['check', true]); + expect(idrsForm.pristine).toBeTrue(); + }); + + it('alerts on failure status and on error', () => { + create(); + const emitted: any[] = []; + component.IDRSChanged.subscribe(v => emitted.push(v)); + doctor.updateIDRSDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'nope' }) + ); + component.updateIDRSDetails(idrsForm, 'x'); + expect(confirmation.alert).toHaveBeenCalledWith('nope', 'error'); + doctor.updateIDRSDetails.and.returnValue(throwingObs('err')); + component.updateIDRSDetails(idrsForm, 'x'); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + expect(emitted).toEqual(['check', false, 'check', false]); + }); + }); + + describe('getIDRSDetailsFrmNurse', () => { + beforeEach(() => { + create(); + component.age = 45; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: null }) + ); + masterData$.next({ IDRSQuestions: QUESTIONS }); + nurse.getPreviousVisitData.calls.reset(); + }); + + it('patches answers and suspects from nurse data', () => { + spyOn(idrsService, 'setSuspectedArrayValue').and.callThrough(); + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { + IDRSDetail: { + suspectedDisease: 'Diabetes,Asthma', + idrsDetails: [ + { idrsQuestionId: 1, answer: 'Yes', ID: 11 }, + { idrsQuestionId: 3, answer: 'No', ID: 13 }, + ], + }, + }, + }) + ); + component.getIDRSDetailsFrmNurse(1, 2); + expect(component.suspect).toEqual(['Diabetes', 'Asthma']); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalled(); + expect(component.revisit).toBeFalse(); + expect(component.questions1[0].answer).toBe('yes'); + expect(component.questions1[0].id).toBe(11); + expect(diseaseOf('Asthma').flag).toBeFalse(); + expect(idrsForm.value.questionArray.length).toBe(2); + expect(idrsForm.value.isDiabetic).toBeTrue(); + expect(component.rev.length).toBe(2); + expect(nurse.getPreviousVisitData).toHaveBeenCalled(); + }); + + it('flags a revisit when no answered question matches', () => { + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { + IDRSDetail: { + suspectedDisease: null, + idrsDetails: [{ idrsQuestionId: 999, answer: 'Yes', ID: 1 }], + }, + }, + }) + ); + component.getIDRSDetailsFrmNurse(1, 2); + expect(component.revisit).toBeTrue(); + expect(idrsForm.value.isDiabetic).toBeFalse(); + }); + + it('never flags a revisit for young beneficiaries', () => { + component.age = 20; + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { + IDRSDetail: { + idrsDetails: [{ idrsQuestionId: 999, answer: 'Yes', ID: 1 }], + }, + }, + }) + ); + component.getIDRSDetailsFrmNurse(1, 2); + expect(component.revisit).toBeFalse(); + }); + + it('handles empty idrs details', () => { + doctor.getIDRSDetails.and.returnValue( + of({ statusCode: 200, data: { IDRSDetail: { idrsDetails: [] } } }) + ); + component.getIDRSDetailsFrmNurse(1, 2); + expect(idrsForm.value.questionArray).toEqual([]); + }); + + it('ignores non-200 responses', () => { + doctor.getIDRSDetails.and.returnValue(of({ statusCode: 500 })); + component.getIDRSDetailsFrmNurse(1, 2); + expect(nurse.getPreviousVisitData).not.toHaveBeenCalled(); + }); + }); + + describe('settingSuspectedObservable', () => { + it('sets flag only once for chronic suspects', () => { + create(); + spyOn(idrsService, 'setSuspectedArrayValue'); + component.suspect = ['Other', 'Malaria Screening', 'Asthma']; + component.settingSuspectedObservable(); + expect(idrsService.setSuspectedArrayValue).toHaveBeenCalledTimes(1); + }); + }); + + describe('previous diabetes history', () => { + beforeEach(() => { + create(); + session.setItem('beneficiaryRegID', 5); + }); + + it('opens the dialog when history exists', () => { + nurse.getPreviousDiabetesHistory.and.returnValue( + of({ statusCode: 200, data: { data: [{ a: 1 }] } }) + ); + component.getPreviousDiabetesHistory(); + expect(nurse.getPreviousDiabetesHistory).toHaveBeenCalledWith( + 5, + 'NCD screening' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: { data: [{ a: 1 }] }, + title: LANGUAGE_EN.previousDiabetesHistoryDetails, + }, + }); + }); + + it('alerts when no history', () => { + nurse.getPreviousDiabetesHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousDiabetesHistory(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.pastDiabetesHistoryNotAvailable + ); + }); + + it('alerts on non-200 and error', () => { + nurse.getPreviousDiabetesHistory.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getPreviousDiabetesHistory(); + nurse.getPreviousDiabetesHistory.and.returnValue(throwingObs()); + component.getPreviousDiabetesHistory(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.errorFetchingHistory, + 'error' + ); + }); + }); + + describe('ngOnDestroy', () => { + it('clears state and unsubscribes', () => { + create(); + spyOn(idrsService, 'clearSystolicBp').and.callThrough(); + spyOn(idrsService, 'clearHypertensionSelected').and.callThrough(); + component.suspect = ['A']; + component.ngOnDestroy(); + expect(component.suspect).toEqual([]); + expect(idrsService.clearSystolicBp).toHaveBeenCalled(); + expect(idrsService.clearHypertensionSelected).toHaveBeenCalled(); + expect(component.idrsWaistSubscription.closed).toBeTrue(); + expect(component.visitDiseaseSubscription.closed).toBeTrue(); + expect(component.diastolicBpValueSubscription.closed).toBeTrue(); + }); + + it('tolerates missing subscriptions', () => { + fixture = TestBed.createComponent(IdrsComponent); + component = fixture.componentInstance; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/ncd-screening/ncd-screening.component.spec.ts b/src/app/app-modules/nurse-doctor/ncd-screening/ncd-screening.component.spec.ts index eec66acc..cd934ff8 100644 --- a/src/app/app-modules/nurse-doctor/ncd-screening/ncd-screening.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/ncd-screening/ncd-screening.component.spec.ts @@ -20,27 +20,497 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { IotcomponentComponent } from '../../core/components/iotcomponent/iotcomponent.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../shared/services'; import { NcdScreeningComponent } from './ncd-screening.component'; describe('NcdScreeningComponent', () => { let component: NcdScreeningComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let confirm: any; + let dialog: any; + let router: Router; + let ben$: BehaviorSubject; + let master$: BehaviorSubject; + const info = LANGUAGE_EN.alerts.info; + + const masterData = () => ({ + bloodPressureStatus: [{ bpAndDiabeticStatusID: 1, name: 'Normal' }], + diabeticStatus: [{ bpAndDiabeticStatusID: 2, name: 'Diabetic' }], + ncdScreeningConditions: [ + { ncdScreeningConditionID: 10, name: 'HTN' }, + { ncdScreeningConditionID: 11, name: 'DM' }, + ], + ncdScreeningReasons: [ + { ncdScreeningReasonID: 1, name: 'Age > 30' }, + { ncdScreeningReasonID: 2, name: 'Symptoms' }, + ], + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup(mode = '') { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + ben$ = new BehaviorSubject(null); + master$ = new BehaviorSubject(null); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [NcdScreeningComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ vanID: 3, parkingPlaceID: 4 }), + visitID: 'v1', + beneficiaryRegID: 'b1', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(NcdScreeningComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + spyOn(console, 'log'); + component.NCDScreeningForm = component.utils.createNCDScreeningForm(); + component.patientMedicalForm = new FormBuilder().group({ + patientVisitForm: new FormBuilder().group({ visitReason: 'r' }), + }); + component.ncdScreeningMode = mode; + component.ngOnInit(); + } + + const form = () => component.NCDScreeningForm; + + afterEach(() => component?.ngOnDestroy()); + + describe('init', () => { + beforeEach(async () => setup()); + + it('sets language, dates and visit form', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.patientVisitForm.value).toEqual({ visitReason: 'r' }); + expect(component.nextScreeningDate.getDate()).toBe( + new Date(new Date().setDate(new Date().getDate() + 1)).getDate() + ); + expect(form().value.vanID).toBe(3); + expect(component.startBPTest).toBe(environment.startBPurl); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('loads master data and filters screening reasons for under-30s', () => { + component.age = 25; + master$.next(masterData()); + expect(component.bloodPressureStatus.length).toBe(1); + expect( + component.ncdScreeningReasons.map((r: any) => r.ncdScreeningReasonID) + ).toEqual([2]); + expect(doctor.getNcdScreeningDetails).not.toHaveBeenCalled(); + component.age = 40; + expect( + component.filterNCDScreeningReasons(masterData().ncdScreeningReasons) + .length + ).toBe(2); + }); + + it('beneficiary details set age/gender and patch visit count', () => { + nurse.getNcdScreeningVisitCount.and.returnValue( + of({ statusCode: 200, data: { ncdScreeningVisitCount: 4 } }) + ); + ben$.next({ ageVal: 45, genderName: 'Female', beneficiaryRegID: 9 }); + expect(nurse.getNcdScreeningVisitCount).toHaveBeenCalledWith(9); + expect(component.age).toBe(45); + expect(component.female).toBeTrue(); + expect(component.ncdScreeningVisitCount).toBe(4); + expect(form().value.ncdScreeningVisitNo).toBe(4); + }); + + it('beneficiary without age / male / failed count', () => { + nurse.getNcdScreeningVisitCount.and.returnValue(of({ statusCode: 500 })); + ben$.next({ genderName: 'Male', beneficiaryRegID: 9 }); + expect(component.age).toBe(0); + expect(component.female).toBeFalse(); + expect(form().value.ncdScreeningVisitNo).toBeNull(); + }); + }); + + it('view mode fetches details and patches mapped values', async () => { + await setup('view'); + nurse.getNcdScreeningVisitCount.and.returnValue( + of({ statusCode: 200, data: { ncdScreeningVisitCount: 4 } }) + ); + ben$.next({ ageVal: 45, beneficiaryRegID: 9 }); + expect(form().value.ncdScreeningVisitNo).toBeNull(); + + doctor.getNcdScreeningDetails.and.returnValue( + of({ + statusCode: 200, + data: { + anthropometryDetails: { height_cm: 170, weight_Kg: 70 }, + ncdScreeningDetails: { + nextScreeningDate: '2024-05-01', + ncdScreeningReasonID: 2, + bloodPressureStatusID: 1, + diabeticStatusID: 2, + isBPPrescribed: true, + isBloodGlucosePrescribed: true, + ncdScreeningConditionList: [{ ncdScreeningConditionID: 11 }], + }, + vitalDetails: { + systolicBP_1stReading: 120, + systolicBP_2ndReading: 130, + systolicBP_3rdReading: 125, + diastolicBP_1stReading: 80, + diastolicBP_2ndReading: 82, + diastolicBP_3rdReading: 84, + }, + }, + }) + ); + master$.next(masterData()); + expect(doctor.getNcdScreeningDetails).toHaveBeenCalledWith('b1', 'v1'); + const v = form().value; + expect(v.reasonForScreening.ncdScreeningReasonID).toBe(2); + expect(v.bloodPressureStatus.name).toBe('Normal'); + expect(v.diabeticStatus.name).toBe('Diabetic'); + expect(v.ncdScreeningConditionList).toEqual([ + { ncdScreeningConditionID: 11, name: 'DM' }, + ]); + expect(v.labTestOrders.map((l: any) => l.procedureName)).toEqual([ + 'BP Measurement', + 'Blood Glucose Measurement', + ]); + expect(v.nextScreeningDate instanceof Date).toBeTrue(); + expect(v.bMI).toBe(24.2); + expect(component.normalBMI).toBeTrue(); + expect(component.checkBloodPressure).toBeTrue(); + expect(component.checkBloodGlucose).toBeTrue(); + expect(v.averageSystolicBP_Reading).toBe('125'); + expect(v.averageDiastolicBP_Reading).toBe('82'); + }); + + it('view mode without prescriptions clears BP / glucose fields', async () => { + await setup('view'); + doctor.getNcdScreeningDetails.and.returnValue( + of({ + statusCode: 200, + data: { + ncdScreeningDetails: { + bloodGlucose_Fasting: 99, + systolicBP_1stReading: 120, + }, + }, + }) + ); + const md: any = masterData(); + md.ncdScreeningConditions = null; + master$.next(md); + const v = form().value; + expect(v.labTestOrders).toEqual([]); + expect(v.ncdScreeningConditionList).toEqual([]); + expect(v.bloodGlucose_Fasting).toBeNull(); + expect(v.systolicBP_1stReading).toBeNull(); + expect(component.checkBloodPressure).toBeFalse(); + expect(component.checkBloodGlucose).toBeFalse(); + expect(v.bMI).toBeNull(); + expect(component.normalBMI).toBeFalse(); + }); + + it('getNCDScreeingDetails ignores non-200', async () => { + await setup(); + doctor.getNcdScreeningDetails.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getNCDScreeingDetails(1, 2); + expect(component.ncdScreeningDetails).toBeUndefined(); + }); + + describe('update', () => { + beforeEach(async () => setup('update')); + + it('notifies mandatory fields when invalid', () => { + ['height_cm', 'weight_Kg', 'isScreeningComplete'].forEach(c => { + form().controls[c].setValidators(Validators.required); + form().controls[c].updateValueAndValidity(); + }); + component.ngOnChanges(); + expect(confirm.notify).toHaveBeenCalledWith(info.mandatoryFields, [ + LANGUAGE_EN.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.height, + LANGUAGE_EN.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.weight, + LANGUAGE_EN.screeningComplete, + ]); + expect(doctor.updateNCDScreeningDetails).not.toHaveBeenCalled(); + }); + + it('updates, alerts success and navigates to worklist', () => { + form().markAsDirty(); + doctor.updateNCDScreeningDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'ok' } }) + ); + component.ngOnChanges(); + expect(doctor.updateNCDScreeningDetails).toHaveBeenCalledWith( + form().value, + { visitReason: 'r' } + ); + expect(form().pristine).toBeTrue(); + expect(confirm.alert).toHaveBeenCalledWith('ok', 'success'); + expect(router.navigate).toHaveBeenCalledWith(['/common/nurse-worklist']); + }); + + it('alerts error on failure response and on error', () => { + doctor.updateNCDScreeningDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.updateNCDScreeningDetails(); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + doctor.updateNCDScreeningDetails.and.returnValue(throwingObs('e')); + component.updateNCDScreeningDetails(); + expect(confirm.alert).toHaveBeenCalledWith('e', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('measurements', () => { + beforeEach(async () => setup()); + + it('calculateBMI flags abnormal BMI', () => { + form().patchValue({ height_cm: 150, weight_Kg: 80 }); + component.calculateBMI(); + expect(component.BMI).toBe(35.6); + expect(form().value.bMI).toBe(35.6); + expect(component.bMI).toBe(35.6); + expect(component.normalBMI).toBeFalse(); + }); + + const rangeChecks: [string, string, number, number, number][] = [ + ['checkHeight', 'height_cm', 150, 110, 210], + ['checkWeight', 'weight_Kg', 60, 20, 160], + ['checkSystolicBP1', 'systolicBP_1stReading', 120, 80, 180], + ['checkSystolicBP2', 'systolicBP_2ndReading', 120, 80, 180], + ['checkSystolicBP3', 'systolicBP_3rdReading', 120, 80, 180], + ['checkDiastolicBP1', 'diastolicBP_1stReading', 80, 40, 120], + ['checkDiastolicBP2', 'diastolicBP_2ndReading', 80, 40, 120], + ['checkDiastolicBP3', 'diastolicBP_3rdReading', 80, 40, 120], + ['checkBloodSugarFasting', 'bloodGlucose_Fasting', 100, 40, 800], + ['checkBloodSugarRandom', 'bloodGlucose_Random', 100, 40, 800], + ['checkBloodSugar2HrPostPrandial', 'bloodGlucose_2hr_PP', 100, 40, 800], + [ + 'checkBloodSugarNotSpecified', + 'bloodGlucose_NotSpecified', + 100, + 40, + 800, + ], + ]; + rangeChecks.forEach(([fn, ctrl, ok, low, high]) => + it(`${fn} alerts only outside the range`, () => { + form().patchValue({ [ctrl]: ok }); + (component as any)[fn](); + expect(confirm.alert).not.toHaveBeenCalled(); + form().patchValue({ [ctrl]: low }); + (component as any)[fn](); + form().patchValue({ [ctrl]: high }); + (component as any)[fn](); + expect(confirm.alert.calls.allArgs()).toEqual([ + [info.recheckValue], + [info.recheckValue], + ]); + }) + ); + + it('hipWaistRatio uses gender specific thresholds', () => { + component.hipWaistRatio(); + expect(component.patientWaistHipRatio).toBeUndefined(); + form().patchValue({ + hipCircumference_cm: 100, + waistCircumference_cm: 85, + }); + component.hipWaistRatio(); + expect(form().value.waistHipRatio).toBe('0.85'); + expect(component.waistHipRatio).toBe('0.85'); + expect(component.normalWaistHipRatio).toBeTrue(); + component.female = true; + component.hipWaistRatio(); + expect(component.normalWaistHipRatio).toBeFalse(); + form().patchValue({ waistCircumference_cm: 70 }); + component.hipWaistRatio(); + expect(component.normalWaistHipRatio).toBeTrue(); + component.female = false; + form().patchValue({ waistCircumference_cm: 95 }); + component.hipWaistRatio(); + expect(component.normalWaistHipRatio).toBeFalse(); + }); + + it('checkHip and checkWaist ranges', () => { + form().patchValue({ hipCircumference_cm: 95 }); + component.checkHip(); + expect(component.normalHip).toBeTrue(); + component.female = true; + component.checkHip(); + expect(component.normalHip).toBeFalse(); + form().patchValue({ hipCircumference_cm: 100 }); + component.checkHip(); + expect(component.normalHip).toBeTrue(); + component.female = false; + form().patchValue({ hipCircumference_cm: 110 }); + component.checkHip(); + expect(component.normalHip).toBeFalse(); + + form().patchValue({ waistCircumference_cm: 80 }); + component.checkWaist(); + expect(component.normalWaist).toBeTrue(); + form().patchValue({ waistCircumference_cm: 40 }); + component.checkWaist(); + expect(component.normalWaist).toBeFalse(); + }); + + it('average BP requires all three readings', () => { + form().patchValue({ + systolicBP_1stReading: 120, + systolicBP_2ndReading: 130, + }); + component.calculateAverageSystolicBP(); + expect(form().value.averageSystolicBP_Reading).toBeNull(); + form().patchValue({ diastolicBP_1stReading: 80 }); + component.calculateAverageDiastolicBP(); + expect(form().value.averageDiastolicBP_Reading).toBeNull(); + form().patchValue({ + diastolicBP_2ndReading: 90, + diastolicBP_3rdReading: 91, + }); + component.calculateAverageDiastolicBP(); + expect(form().value.averageDiastolicBP_Reading).toBe('87'); + expect(component.averageDiastolicBP_Reading).toBe('87'); + expect(component.averageSystolicBP_Reading).toBe('87'); + }); + + [1, 2, 3].forEach(n => { + const suffix = n === 1 ? '1st' : n === 2 ? '2nd' : '3rd'; + it(`checkSystolicGreater${n} clears systolic ${suffix} reading when <= diastolic`, () => { + const sys = `systolicBP_${suffix}Reading`; + form().patchValue({ [sys]: 80 }); + (component as any)[`checkSystolicGreater${n}`](80, 90); + expect(confirm.alert).toHaveBeenCalledWith(info.sysBp); + expect(form().value[sys]).toBeNull(); + confirm.alert.calls.reset(); + form().patchValue({ [sys]: 120 }); + (component as any)[`checkSystolicGreater${n}`](120, 80); + (component as any)[`checkSystolicGreater${n}`](null, 80); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(form().value[sys]).toBe(120); + }); + + it(`checkDiastolicLesser${n} clears diastolic ${suffix} reading when >= systolic`, () => { + const dia = `diastolicBP_${suffix}Reading`; + form().patchValue({ [dia]: 100 }); + (component as any)[`checkDiastolicLesser${n}`](90, 100); + expect(confirm.alert).toHaveBeenCalledWith(info.DiaBp); + expect(form().value[dia]).toBeNull(); + confirm.alert.calls.reset(); + form().patchValue({ [dia]: 70 }); + (component as any)[`checkDiastolicLesser${n}`](120, 70); + (component as any)[`checkDiastolicLesser${n}`](120, null); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(form().value[dia]).toBe(70); + }); + }); + }); + + describe('IOT dialogs', () => { + beforeEach(async () => setup()); + + const openWith = (result: any) => + dialog.open.and.returnValue(createDialogRefMock(result)); + + it('weight dialog patches weight and recalculates BMI', () => { + form().patchValue({ height_cm: 100 }); + openWith({ result: 20 }); + component.openIOTWeightModel(); + expect(dialog.open).toHaveBeenCalledWith(IotcomponentComponent, { + width: '600px', + height: '180px', + disableClose: true, + data: { startAPI: environment.startWeighturl }, + }); + expect(form().value.weight_Kg).toBe(20); + expect(form().value.bMI).toBe(20); + }); + + const bg: [string, string][] = [ + ['openIOTBGFastingModel', 'bloodGlucose_Fasting'], + ['openIOTBGRandomModel', 'bloodGlucose_Random'], + ['openIOTBGPostPrandialModel', 'bloodGlucose_2hr_PP'], + ['openIOTBGNotSpecifiedModel', 'bloodGlucose_NotSpecified'], + ]; + bg.forEach(([fn, ctrl]) => + it(`${fn} patches ${ctrl}`, () => { + openWith({ result: 140 }); + (component as any)[fn](); + expect(dialog.open.calls.mostRecent().args[1].data).toEqual({ + startAPI: environment.startBloodGlucoseurl, + }); + expect(form().value[ctrl]).toBe(140); + }) + ); + + ['1st', '2nd', '3rd'].forEach((s, i) => + it(`openIOTBP${i + 1}Model patches ${s} readings`, () => { + openWith({ sys: 121, dia: 81 }); + (component as any)[`openIOTBP${i + 1}Model`](); + expect(dialog.open.calls.mostRecent().args[1].data).toEqual({ + startAPI: environment.startBPurl, + }); + expect(form().value[`systolicBP_${s}Reading`]).toBe(121); + expect(form().value[`diastolicBP_${s}Reading`]).toBe(81); + }) + ); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', async () => { + await setup(); + const a = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + const b = spyOn(component.beneficiaryDetailSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-reffered-worklist/nurse-reffered-worklist.component.spec.ts b/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-reffered-worklist/nurse-reffered-worklist.component.spec.ts index d0ceeea9..cef6cb71 100644 --- a/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-reffered-worklist/nurse-reffered-worklist.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-reffered-worklist/nurse-reffered-worklist.component.spec.ts @@ -20,27 +20,282 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; import { NurseRefferedWorklistComponent } from './nurse-reffered-worklist.component'; +import { NurseService } from '../../shared/services'; +import { + BeneficiaryDetailsService, + CameraService, + ConfirmationService, +} from 'src/app/app-modules/core/services'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; describe('NurseRefferedWorklistComponent', () => { - let component: NurseRefferedWorklistComponent; let fixture: ComponentFixture; + let component: NurseRefferedWorklistComponent; + let nurseService: any; + let cameraService: any; + let benDetails: any; + let confirmation: any; + let session: any; + let router: Router; + let removeSpy: jasmine.Spy; + let setSpy: jasmine.Spy; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const makeList = () => [ + { + beneficiaryID: 101, + benName: 'Asha', + genderName: 'Female', + VisitCategory: 'NCD screening', + benVisitNo: 1, + beneficiaryRegID: 1, + benFlowID: 10, + benVisitID: 100, + visitCode: 1000, + }, + { + beneficiaryID: 202, + benName: 'Bala', + genderName: 'Male', + VisitCategory: 'General OPD', + benVisitNo: 3, + beneficiaryRegID: 2, + benFlowID: 20, + benVisitID: 200, + visitCode: 2000, + }, + ]; + + beforeEach(async () => { + nurseService = autoSpy(NurseService); + nurseService.getNurseWorklistTMreferred.and.returnValue( + of({ statusCode: 200, data: makeList() }) + ); + cameraService = autoSpy(CameraService); + benDetails = autoSpy(BeneficiaryDetailsService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [NurseRefferedWorklistComponent], + providers: [ + ...commonTestProviders(), + { provide: NurseService, useValue: nurseService }, + { provide: CameraService, useValue: cameraService }, + { provide: BeneficiaryDetailsService, useValue: benDetails }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(NurseRefferedWorklistComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + removeSpy = spyOn(Storage.prototype, 'removeItem'); + setSpy = spyOn(Storage.prototype, 'setItem'); + spyOn(console, 'log'); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('ngOnInit / loadWorklist', () => { + it('sets the doctor role, clears visit data and loads the TM referred worklist', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(session.setItem).toHaveBeenCalledWith('currentRole', 'Doctor'); + expect(removeSpy).toHaveBeenCalledWith('tmCaseSheet'); + expect(removeSpy).toHaveBeenCalledWith('visitCat'); + expect(removeSpy).toHaveBeenCalledWith('disableNoOnSuccessOfYes'); + expect(nurseService.getNurseWorklistTMreferred).toHaveBeenCalled(); + }); + + it('keeps all rows in the table but filters the list to NCD screening', () => { + expect(component.dataSource.data.length).toBe(2); + expect(component.dataSource.data.map((r: any) => r.sno)).toEqual([1, 2]); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe(101); + expect(component.currentPage).toBe(1); + expect(component.filterTerm).toBeNull(); + }); + + it('fills missing fields with "Not Available"', () => { + nurseService.getNurseWorklistTMreferred.and.returnValue( + of({ statusCode: 200, data: [{}] }) + ); + component.loadWorklist(); + const row = component.beneficiaryList[0]; + expect(row.benFlowID).toBe('Not Available'); + expect(row.genderName).toBe('Not Available'); + expect(row.isTMVisitDone).toBe('Not Available'); + expect(typeof row.visitDate).toBe('string'); + }); + + it('alerts on a non-200 response', () => { + nurseService.getNurseWorklistTMreferred.and.returnValue( + of({ statusCode: 500, errorMessage: 'err' }) + ); + component.loadWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + }); + }); + + describe('filterBeneficiaryList', () => { + it('restores the full list for an empty term', () => { + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + expect(component.dataSource.data.length).toBe(2); + }); + + it('filters on searchable fields', () => { + component.filterBeneficiaryList('bala'); + expect( + component.filteredBeneficiaryList.map((b: any) => b.beneficiaryID) + ).toEqual([202]); + }); + + it('matches visit number labels', () => { + component.beneficiaryList = [{ benVisitNo: 1 }, { benVisitNo: 2 }]; + component.filterBeneficiaryList('first visit'); + expect(component.filteredBeneficiaryList).toEqual([ + jasmine.objectContaining({ benVisitNo: 1 }), + ]); + component.filterBeneficiaryList('revisit'); + expect(component.filteredBeneficiaryList).toEqual([ + jasmine.objectContaining({ benVisitNo: 2 }), + ]); + }); + }); + + it('pageChanged slices the filtered list', () => { + component.filteredBeneficiaryList = [1, 2, 3]; + component.pageChanged({ page: 2, itemsPerPage: 2 }); + expect(component.pagedList as any[]).toEqual([3]); + }); + + describe('patientImageView', () => { + it('views the image when available', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({ benImage: 'b64' })); + component.patientImageView(1); + expect(cameraService.viewImage).toHaveBeenCalledWith('b64'); + }); + + it('alerts when the image is missing', () => { + benDetails.getBeneficiaryImage.and.returnValue(of(null)); + component.patientImageView(1); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.imageNotFound + ); + }); + }); + + describe('loadNursePatientDetails', () => { + it('stores details and navigates to the nurse page for specialist_flag 100', () => { + const ben = { ...makeList()[0], specialist_flag: 100 }; + component.loadNursePatientDetails(ben); + expect(session.setItem).toHaveBeenCalledWith('visitCode', 1000); + expect(session.setItem).toHaveBeenCalledWith('visitID', 100); + expect(session.setItem).toHaveBeenCalledWith('specialist_flag', 100); + expect(session.setItem).toHaveBeenCalledWith( + 'beneficiaryData', + JSON.stringify(ben) + ); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/attendant/nurse/patient/', + 1, + ]); + }); + + it('does not navigate when the user cancels', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadNursePatientDetails({ + ...makeList()[0], + specialist_flag: 100, + }); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('opens the TM case sheet for specialist_flag 200', () => { + nurseService.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 200, data: { a: 1 } }) + ); + const ben = { ...makeList()[1], specialist_flag: 200 }; + component.loadNursePatientDetails(ben); + expect(setSpy).toHaveBeenCalledWith('tmCaseSheet', 'true'); + expect(session.setItem).toHaveBeenCalledWith('caseSheetTMFlag', 'true'); + expect(nurseService.getTMReferredCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'General OPD', + benFlowID: 20, + benVisitID: 200, + beneficiaryRegID: 2, + visitCode: 2000, + }); + expect(component.visitCategory).toBe('General OPD'); + expect(component.caseSheetData).toEqual({ a: 1 }); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + }); + + it('does nothing for other specialist flags', () => { + component.loadNursePatientDetails({ specialist_flag: 1 }); + expect(confirmation.confirm).not.toHaveBeenCalled(); + expect(nurseService.getTMReferredCasesheetData).not.toHaveBeenCalled(); + }); + }); + + describe('viewAndPrintCaseSheet', () => { + it('does not fetch when neither TM flag nor specialist flag 200 is set', () => { + spyOn(component, 'setCasesheetData'); + component.viewAndPrintCaseSheet({}); + expect(nurseService.getTMReferredCasesheetData).not.toHaveBeenCalled(); + }); + + it('fetches when the specialist flag is 200', () => { + spyOn(component, 'setCasesheetData'); + session.store.set('specialistFlag', '200'); + component.viewAndPrintCaseSheet({}); + expect(nurseService.getTMReferredCasesheetData).toHaveBeenCalled(); + }); + }); + + describe('getTMReferredCasesheetData', () => { + it('does not route when the consultation confirm is declined', () => { + nurseService.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + confirmation.confirm.and.returnValue(of(false)); + component.getTMReferredCasesheetData({}); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.consulation + ); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts the error message on a failed response', () => { + nurseService.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 500, errorMessage: 'nope' }) + ); + component.getTMReferredCasesheetData({}); + expect(confirmation.alert).toHaveBeenCalledWith('nope', 'error'); + }); + + it('alerts a generic message on HTTP error', () => { + nurseService.getTMReferredCasesheetData.and.returnValue(throwingObs()); + component.getTMReferredCasesheetData({}); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Error in fetching TM Casesheet', + 'error' + ); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-worklist-tabs.component.spec.ts b/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-worklist-tabs.component.spec.ts index 8611fbc4..c39b8ec8 100644 --- a/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-worklist-tabs.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/nurse-worklist-tabs/nurse-worklist-tabs.component.spec.ts @@ -20,17 +20,35 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; import { NurseWorklistTabsComponent } from './nurse-worklist-tabs.component'; +import { HttpServiceService } from '../../core/services/http-service.service'; describe('NurseWorklistTabsComponent', () => { let component: NurseWorklistTabsComponent; let fixture: ComponentFixture; + let mockHttpService: jasmine.SpyObj; + + const mockLanguageObject = { reports: 'Reports', worklist: 'Worklist' }; + + beforeEach(waitForAsync(() => { + mockHttpService = jasmine.createSpyObj( + 'HttpServiceService', + ['fetchLanguageSet'], + { + currentLangugae$: new BehaviorSubject( + mockLanguageObject + ).asObservable(), + } + ); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [NurseWorklistTabsComponent], + providers: [{ provide: HttpServiceService, useValue: mockHttpService }], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -43,4 +61,32 @@ describe('NurseWorklistTabsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should call assignSelectedLanguage on init', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngOnInit(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('ngDoCheck', () => { + it('should call assignSelectedLanguage on doCheck', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngDoCheck(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('assignSelectedLanguage', () => { + it('should set currentLanguageSet from SetLanguageComponent', () => { + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(mockLanguageObject); + }); + + it('should update currentLanguageSet when language changes', () => { + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toBeDefined(); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/nurse-worklist/nurse-worklist.component.spec.ts b/src/app/app-modules/nurse-doctor/nurse-worklist/nurse-worklist.component.spec.ts index 308c3f97..0748d960 100644 --- a/src/app/app-modules/nurse-doctor/nurse-worklist/nurse-worklist.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/nurse-worklist/nurse-worklist.component.spec.ts @@ -20,73 +20,264 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; +import { of } from 'rxjs'; -import { Md2Module } from 'md2'; -import { MaterialModule } from '../../../app-modules/core/material.module'; import { NurseWorklistComponent } from './nurse-worklist.component'; - +import { NurseService } from '../shared/services'; +import { CameraService } from '../../core/services/camera.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { - CameraService, - BeneficiaryDetailsService, - ConfirmationService, -} from '../../../app-modules/core/services'; -import { MasterdataService, NurseService } from '../shared/services'; - -import { nurseWorklist } from '../shared/mocks/mock-data'; -import { RouterStub } from '../shared/mocks/router-stub'; -import { NurseServiceStub } from '../shared/mocks/nurse-service-stub'; -import { MasterdataServiceStub } from '../shared/mocks/masterdata-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../app-modules/core/mocks/beneficiary-details-service-stub'; - -import { Observable } from 'rxjs/Rx'; + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; describe('NurseWorklistComponent', () => { let component: NurseWorklistComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let nurseService: NurseService; + let nurseService: any; + let cameraService: any; + let benDetails: any; + let confirmation: any; + let session: any; + let router: Router; + + const makeList = () => [ + { + beneficiaryID: 111, + benName: 'Ravi Kumar', + genderName: 'Male', + fatherName: 'Suresh', + districtName: 'Pune', + villageName: 'Baner', + preferredPhoneNum: '9999999999', + benVisitNo: 1, + beneficiaryRegID: 11, + benFlowID: 1, + age: 30, + }, + { + beneficiaryID: 222, + benName: 'Sita Devi', + genderName: 'Female', + fatherName: 'Ram', + districtName: 'Nashik', + villageName: 'Panchvati', + preferredPhoneNum: '8888888888', + benVisitNo: 2, + beneficiaryRegID: 22, + benFlowID: 2, + age: 25, + }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, Md2Module, MaterialModule], + beforeEach(async () => { + nurseService = autoSpy(NurseService); + nurseService.getNurseWorklist.and.returnValue( + of({ statusCode: 200, data: makeList() }) + ); + cameraService = autoSpy(CameraService); + benDetails = autoSpy(BeneficiaryDetailsService); + + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [NurseWorklistComponent], providers: [ - { - provide: Router, - useClass: RouterStub, - }, - { - provide: NurseService, - useClass: NurseServiceStub, - }, - { - provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, - }, - CameraService, - ConfirmationService, + ...commonTestProviders(), + { provide: NurseService, useValue: nurseService }, + { provide: CameraService, useValue: cameraService }, + { provide: BeneficiaryDetailsService, useValue: benDetails }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(NurseWorklistComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - nurseService = debugElement.injector.get(NurseService); + confirmation = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + spyOn(Storage.prototype, 'removeItem'); + spyOn(console, 'log'); + fixture.detectChanges(); + }); - spyOn(nurseService, 'getNurseWorklist').and.returnValue( - Observable.of(nurseWorklist) - ); + afterEach(() => fixture.destroy()); - fixture.detectChanges(); + describe('ngOnInit', () => { + it('sets role, clears visit data, loads the worklist and resets beneficiary details', () => { + expect(session.setItem).toHaveBeenCalledWith('currentRole', 'Nurse'); + expect(Storage.prototype.removeItem).toHaveBeenCalledWith('visitCode'); + expect(Storage.prototype.removeItem).toHaveBeenCalledWith( + 'caseSheetTMFlag' + ); + expect(nurseService.getNurseWorklist).toHaveBeenCalled(); + expect(benDetails.reset).toHaveBeenCalled(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + }); + + describe('getNurseWorklist', () => { + it('populates lists, numbers rows and clears the filter on success', () => { + expect(component.beneficiaryList.length).toBe(2); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + expect(component.dataSource.data.map((d: any) => d.sno)).toEqual([1, 2]); + expect(component.filterTerm).toBeNull(); + }); + + it('fills missing fields with "Not Available"', () => { + nurseService.getNurseWorklist.and.returnValue( + of({ statusCode: 200, data: [{ beneficiaryID: 1 }] }) + ); + component.getNurseWorklist(); + const row = component.beneficiaryList[0]; + expect(row.genderName).toBe('Not Available'); + expect(row.age).toBe('Not Available'); + expect(row.benVisitNo).toBe('Not Available'); + expect(row.districtName).toBe('Not Available'); + expect(row.villageName).toBe('Not Available'); + expect(row.fatherName).toBe('Not Available'); + expect(row.preferredPhoneNum).toBe('Not Available'); + }); + + it('alerts the error message and empties the table on a non-200 response', () => { + nurseService.getNurseWorklist.and.returnValue( + of({ statusCode: 5000, errorMessage: 'failed' }) + ); + component.getNurseWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith('failed', 'error'); + expect(component.dataSource.data).toEqual([]); + }); + + it('alerts when data is null', () => { + nurseService.getNurseWorklist.and.returnValue( + of({ statusCode: 200, data: null, errorMessage: 'no data' }) + ); + component.getNurseWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith('no data', 'error'); + }); + + it('alerts the error on an unhandled HTTP failure', () => { + const err = { status: 500 }; + nurseService.getNurseWorklist.and.returnValue(throwingObs(err)); + component.getNurseWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith(err, 'error'); + }); + + it('does not alert when the error was already handled', () => { + nurseService.getNurseWorklist.and.returnValue( + throwingObs({ handled: true }) + ); + confirmation.alert.calls.reset(); + component.getNurseWorklist(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + }); + + describe('ngOnDestroy', () => { + it('removes currentRole', () => { + component.ngOnDestroy(); + expect(Storage.prototype.removeItem).toHaveBeenCalledWith('currentRole'); + }); + }); + + describe('pageChanged', () => { + it('slices the filtered list for the requested page', () => { + component.filteredBeneficiaryList = [1, 2, 3, 4, 5]; + component.pageChanged({ page: 2, itemsPerPage: 2 }); + expect(component.pagedList).toEqual([3, 4]); + }); + }); + + describe('patientImageView', () => { + it('shows the image when one is returned', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({ benImage: 'img' })); + component.patientImageView(11); + expect(benDetails.getBeneficiaryImage).toHaveBeenCalledWith(11); + expect(cameraService.viewImage).toHaveBeenCalledWith('img'); + }); + + it('alerts image-not-found otherwise', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({})); + component.patientImageView(11); + expect(cameraService.viewImage).not.toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.imageNotFound + ); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('loadNursePatientDetails', () => { + const ben = makeList()[0]; + + it('stores beneficiary details and navigates when confirmed', () => { + component.loadNursePatientDetails(ben); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.confirmtoProceedFurther + ); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryGender', 'Male'); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryRegID', 11); + expect(session.setItem).toHaveBeenCalledWith('benFlowID', 1); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryID', 111); + expect(session.setItem).toHaveBeenCalledWith('benVisitNo', 1); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/attendant/nurse/patient/', + 11, + ]); + }); + + it('does nothing when the user cancels', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadNursePatientDetails(ben); + expect(router.navigate).not.toHaveBeenCalled(); + expect(session.setItem).not.toHaveBeenCalledWith('benFlowID', 1); + }); + }); + + describe('filterBeneficiaryList', () => { + it('restores the full list for an empty term', () => { + component.filteredBeneficiaryList = []; + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + expect(component.dataSource.data.length).toBe(2); + expect(component.dataSource.data[1].sno).toBe(2); + }); + + it('matches on a searchable field case-insensitively', () => { + component.filterBeneficiaryList('SITA'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe(222); + expect(component.dataSource.data[0].sno).toBe(1); + }); + + it('matches "first visit" against benVisitNo 1', () => { + component.beneficiaryList = [{ benVisitNo: 1 }, { benVisitNo: 2 }]; + component.filterBeneficiaryList('first'); + expect(component.filteredBeneficiaryList).toEqual([ + jasmine.objectContaining({ benVisitNo: 1 }), + ]); + }); + + it('matches "revisit" against other visit numbers', () => { + component.beneficiaryList = [{ benVisitNo: 1 }, { benVisitNo: 2 }]; + component.filterBeneficiaryList('revisit'); + expect(component.filteredBeneficiaryList).toEqual([ + jasmine.objectContaining({ benVisitNo: 2 }), + ]); + }); + + it('returns nothing when no field matches', () => { + component.filterBeneficiaryList('zzz-no-match'); + expect(component.filteredBeneficiaryList).toEqual([]); + expect(component.dataSource.data).toEqual([]); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/oncologist-worklist/oncologist-worklist.component.spec.ts b/src/app/app-modules/nurse-doctor/oncologist-worklist/oncologist-worklist.component.spec.ts index 981d1ae4..dd8e8604 100644 --- a/src/app/app-modules/nurse-doctor/oncologist-worklist/oncologist-worklist.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/oncologist-worklist/oncologist-worklist.component.spec.ts @@ -20,27 +20,232 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; import { OncologistWorklistComponent } from './oncologist-worklist.component'; +import { DoctorService } from '../shared/services/doctor.service'; +import { CameraService } from '../../core/services/camera.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; describe('OncologistWorklistComponent', () => { - let component: OncologistWorklistComponent; let fixture: ComponentFixture; + let component: OncologistWorklistComponent; + let doctorService: any; + let cameraService: any; + let benDetails: any; + let confirmation: any; + let session: any; + let router: Router; + let removeSpy: jasmine.Spy; + + const makeList = () => [ + { + beneficiaryID: 11, + benName: 'Kavya', + genderName: 'Female', + age: '30 Years', + VisitCategory: 'Cancer Screening', + districtName: 'Pune', + beneficiaryRegID: 1, + benFlowID: 5, + benVisitID: 6, + visitCode: 7, + doctorFlag: 1, + nurseFlag: 9, + pharmacist_flag: 0, + visitFlowStatusFlag: 'N', + visitDate: '2024-01-02T10:00:00', + }, + { + beneficiaryID: 22, + benName: 'Mohan', + genderName: 'Male', + beneficiaryRegID: 2, + benFlowID: 8, + benVisitID: 9, + visitCode: 10, + VisitCategory: 'NCD screening', + visitFlowStatusFlag: 'D', + }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + doctorService = autoSpy(DoctorService); + doctorService.getOncologistWorklist.and.returnValue( + of({ statusCode: 200, data: makeList() }) + ); + cameraService = autoSpy(CameraService); + benDetails = autoSpy(BeneficiaryDetailsService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [OncologistWorklistComponent], + providers: [ + ...commonTestProviders(), + { provide: DoctorService, useValue: doctorService }, + { provide: CameraService, useValue: cameraService }, + { provide: BeneficiaryDetailsService, useValue: benDetails }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(OncologistWorklistComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + removeSpy = spyOn(Storage.prototype, 'removeItem'); + spyOn(console, 'log'); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('ngOnInit / loadWorklist', () => { + it('sets the role, clears visit data and loads the worklist', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(session.setItem).toHaveBeenCalledWith('currentRole', 'Oncologist'); + expect(removeSpy).toHaveBeenCalledWith('visitCode'); + expect(removeSpy).toHaveBeenCalledWith('caseSheetTMFlag'); + expect(doctorService.getOncologistWorklist).toHaveBeenCalled(); + expect(component.dataSource.data.map((r: any) => r.sno)).toEqual([1, 2]); + expect(component.filterTerm).toBeNull(); + }); + + it('formats dates and fills missing fields', () => { + const [first, second] = component.beneficiaryList; + expect(first.visitDate).toBe('02-01-2024 10:00 AM '); + expect(second.age).toBe('Not Available'); + expect(second.districtName).toBe('Not Available'); + expect(second.statusMessage).toBe('Not Available'); + expect(second.preferredPhoneNum).toBe('Not Available'); + }); + + it('alerts and clears the table on a failed response', () => { + doctorService.getOncologistWorklist.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.loadWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(component.dataSource.data).toEqual([]); + }); + + it('alerts on HTTP error', () => { + const err = { status: 0 }; + doctorService.getOncologistWorklist.and.returnValue(throwingObs(err)); + component.loadWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith(err, 'error'); + }); + }); + + describe('filterBeneficiaryList', () => { + it('restores the full list for an empty term', () => { + component.filteredBeneficiaryList = []; + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + }); + + it('filters on searchable fields and renumbers the table', () => { + component.filterBeneficiaryList('MOHAN'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.dataSource.data[0].beneficiaryID).toBe(22); + expect(component.dataSource.data[0].sno).toBe(1); + }); + + it('returns an empty list when nothing matches', () => { + component.filterBeneficiaryList('xyz-none'); + expect(component.filteredBeneficiaryList).toEqual([]); + expect(component.dataSource.data).toEqual([]); + }); + }); + + it('pageChanged slices the filtered list', () => { + component.filteredBeneficiaryList = [1, 2, 3, 4]; + component.pageChanged({ page: 2, itemsPerPage: 3 }); + expect(component.pagedList as any[]).toEqual([4]); + }); + + describe('patientImageView', () => { + it('views the image when available', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({ benImage: 'img' })); + component.patientImageView(1); + expect(benDetails.getBeneficiaryImage).toHaveBeenCalledWith(1); + expect(cameraService.viewImage).toHaveBeenCalledWith('img'); + }); + + it('alerts when there is no image', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({})); + component.patientImageView(1); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.imageNotFound + ); + }); + }); + + describe('loadDoctorExaminationPage', () => { + it('stores visit details and opens the patient page for a new visit', () => { + const ben = makeList()[0]; + component.loadDoctorExaminationPage(ben); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.confirmtoProceedFurther + ); + expect(session.setItem).toHaveBeenCalledWith('visitCode', 7); + expect(session.setItem).toHaveBeenCalledWith('benFlowID', 5); + expect(session.setItem).toHaveBeenCalledWith('visitID', 6); + expect(session.setItem).toHaveBeenCalledWith('doctorFlag', 1); + expect(session.setItem).toHaveBeenCalledWith('nurseFlag', 9); + expect(session.setItem).toHaveBeenCalledWith('pharmacist_flag', 0); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryRegID', 1); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryID', 11); + expect(session.setItem).toHaveBeenCalledWith( + 'visitCategory', + 'Cancer Screening' + ); + expect(router.navigate).toHaveBeenCalledWith(['/common/patient', 1]); + }); + + it('does not navigate when a new visit is not confirmed', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage(makeList()[0]); + expect(router.navigate).not.toHaveBeenCalled(); + expect(session.setItem).not.toHaveBeenCalledWith('visitID', 6); + }); + + it('opens the case sheet for a completed visit', () => { + component.loadDoctorExaminationPage(makeList()[1]); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.consulation + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetBenFlowID', 8); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategory', + 'NCD screening' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetBeneficiaryRegID', + 2 + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetVisitID', 9); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + }); + + it('does not open the case sheet when declined', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage(makeList()[1]); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/pnc/pnc.component.spec.ts b/src/app/app-modules/nurse-doctor/pnc/pnc.component.spec.ts index 6d6b08e8..92e916e5 100644 --- a/src/app/app-modules/nurse-doctor/pnc/pnc.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/pnc/pnc.component.spec.ts @@ -20,27 +20,324 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../shared/services'; import { PncComponent } from './pnc.component'; describe('PncComponent', () => { let component: PncComponent; let fixture: ComponentFixture; + let doctor: any; + let confirm: any; + let ben$: BehaviorSubject; + let master$: BehaviorSubject; + + const fields = [ + 'deliveryPlace', + 'otherDeliveryPlace', + 'deliveryType', + 'deliveryComplication', + 'otherDeliveryComplication', + 'pregOutcome', + 'postNatalComplication', + 'otherPostNatalComplication', + 'gestationName', + 'birthWeightOfNewborn', + 'newBornHealthStatus', + 'dateOfDelivery', + 'dDate', + ]; + const makeForm = () => { + const g: any = {}; + fields.forEach(f => (g[f] = new FormControl(null))); + return new FormGroup(g); + }; + + const masterData = () => ({ + deliveryTypes: [ + { deliveryType: 'Normal Delivery' }, + { deliveryType: 'Assisted Delivery' }, + { deliveryType: 'Cesarean Section (LSCS)' }, + ], + deliveryPlaces: [ + { deliveryPlace: 'Home-Supervised' }, + { deliveryPlace: 'Hospital' }, + ], + deliveryComplicationTypes: [{ deliveryComplicationType: 'None' }], + pregOutcomes: [{ pregOutcome: 'Live Birth' }], + postNatalComplications: [{ complicationValue: 'Fever' }], + gestation: [{ name: 'Term' }], + newbornHealthStatuses: [{ newBornHealthStatus: 'Healthy' }], + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup(mode?: string, master: any = null) { + doctor = autoSpy(DoctorService); + ben$ = new BehaviorSubject(null); + master$ = new BehaviorSubject(master); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [PncComponent], + providers: [ + ...commonTestProviders({ + session: { + visitID: 'v1', + beneficiaryRegID: 'b1', + providerServiceID: 'p1', + userName: 'u', + visitCode: 'c1', + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: autoSpy(NurseService) }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + nurseMasterData$: master$.asObservable(), + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: ben$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(PncComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + component.patientPNCDataForm = makeForm(); + component.mode = mode as any; + spyOn(console, 'log'); + } + + afterEach(() => component?.ngOnDestroy()); + + it('ngOnInit sets language, dates and waits for master data', async () => { + await setup(); + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + const diff = + component.today.getTime() - component.minimumDeliveryDate.getTime(); + expect(diff).toBe(365 * 24 * 60 * 60 * 1000); + expect(component.masterData).toBeUndefined(); + master$.next({ foo: 1 }); + expect(component.masterData).toBeUndefined(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('stores master data without patching when no mode', async () => { + await setup(undefined, masterData()); + component.ngOnInit(); + expect(component.selectDeliveryTypes.length).toBe(3); + expect(doctor.getPNCDetails).not.toHaveBeenCalled(); + }); + + it('beneficiary details compute dob when not in a mode', async () => { + await setup(); + component.ngOnInit(); + ben$.next({ ageVal: 30 }); + expect(component.beneficiaryAge).toBe(30); + expect(component.dob.getFullYear()).toBe(new Date().getFullYear() - 30); + }); + + it('beneficiary details skip dob in view mode', async () => { + await setup('view'); + component.ngOnInit(); + const dob = component.dob; + ben$.next({ ageVal: 30 }); + expect(component.beneficiaryAge).toBe(30); + expect(component.dob).toBe(dob); + }); + + it('patches PNC details with master data lookups in view mode', async () => { + await setup('view'); + doctor.getPNCDetails.and.returnValue( + of({ + statusCode: 200, + data: { + PNCCareDetail: { + deliveryType: 'Normal Delivery', + deliveryPlace: 'Hospital', + deliveryComplication: 'None', + pregOutcome: 'Live Birth', + postNatalComplication: 'Fever', + gestationName: 'Term', + newBornHealthStatus: 'Healthy', + dateOfDelivery: '2024-02-10T00:00:00', + birthWeightOfNewborn: 3, + }, + }, + }) + ); + component.ngOnInit(); + master$.next(masterData()); + expect(doctor.getPNCDetails).toHaveBeenCalledWith('b1', 'v1'); + const v = component.patientPNCDataForm.value; + expect(v.deliveryType).toEqual({ deliveryType: 'Normal Delivery' }); + expect(v.deliveryPlace).toEqual({ deliveryPlace: 'Hospital' }); + expect(v.deliveryComplication).toEqual({ + deliveryComplicationType: 'None', + }); + expect(v.pregOutcome).toEqual({ pregOutcome: 'Live Birth' }); + expect(v.postNatalComplication).toEqual({ complicationValue: 'Fever' }); + expect(v.gestationName).toEqual({ name: 'Term' }); + expect(v.newBornHealthStatus).toEqual({ newBornHealthStatus: 'Healthy' }); + expect(v.dDate).toBe('2024-02-10T00:00:00.000Z'); + expect(v.birthWeightOfNewborn).toBe(3); + }); + + it('patchDataToFields keeps raw values when master lists are missing', async () => { + await setup('view'); + component.masterData = { deliveryTypes: [] }; + doctor.getPNCDetails.and.returnValue( + of({ + data: { + PNCCareDetail: { deliveryPlace: 'Hospital', dateOfDelivery: null }, + }, + }) + ); + component.patchDataToFields('b', 'v'); + const v = component.patientPNCDataForm.value; + expect(v.deliveryPlace).toBe('Hospital'); + expect(v.deliveryType).toBeUndefined(); + expect(v.dDate).toMatch(/^19(69|70)-/); + }); + + describe('ngOnChanges / updatePatientPNC', () => { + it('does nothing for view or undefined mode', async () => { + await setup('view'); + component.ngOnChanges(); + component.mode = undefined as any; + component.ngOnChanges(); + expect(doctor.updatePNCDetails).not.toHaveBeenCalled(); + }); + + it('updates with normalised delivery date and alerts success', async () => { + await setup('update'); + component.patientPNCDataForm.patchValue({ + dDate: new Date(2024, 5, 3, 15), + }); + component.patientPNCDataForm.markAsDirty(); + doctor.updatePNCDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + component.ngOnChanges(); + expect(doctor.updatePNCDetails).toHaveBeenCalledWith( + component.patientPNCDataForm, + { + beneficiaryRegID: 'b1', + benVisitID: 'v1', + providerServiceMapID: 'p1', + modifiedBy: 'u', + visitCode: 'c1', + } + ); + expect(component.patientPNCDataForm.value.dateOfDelivery).toBe( + '2024-06-03T00:00:00.000Z' + ); + expect(confirm.alert).toHaveBeenCalledWith('saved', 'success'); + expect(component.patientPNCDataForm.pristine).toBeTrue(); + }); + + it('alerts error on failure response and on error', async () => { + await setup('update'); + doctor.updatePNCDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.updatePatientPNC(component.patientPNCDataForm); + expect(confirm.alert).toHaveBeenCalledWith('bad', 'error'); + expect(component.patientPNCDataForm.value.dateOfDelivery).toBeNull(); + doctor.updatePNCDetails.and.returnValue(throwingObs('boom')); + component.updatePatientPNC(component.patientPNCDataForm); + expect(confirm.alert).toHaveBeenCalledWith('boom', 'error'); + }); + }); + + it('checkWeight alerts only for weight >= 6', async () => { + await setup(); + component.ngOnInit(); + component.patientPNCDataForm.patchValue({ birthWeightOfNewborn: 5.9 }); + component.checkWeight(); + expect(confirm.alert).not.toHaveBeenCalled(); + component.patientPNCDataForm.patchValue({ birthWeightOfNewborn: 6 }); + component.checkWeight(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.recheckValue + ); + }); + + it('resetOtherPlaceOfDelivery filters delivery types for home deliveries', async () => { + await setup(undefined, masterData()); + component.ngOnInit(); + component.patientPNCDataForm.patchValue({ + deliveryPlace: { deliveryPlace: 'Home-Supervised' }, + otherDeliveryPlace: 'x', + deliveryType: 'y', + }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes).toEqual([ + { deliveryType: 'Normal Delivery' }, + ]); + expect(component.patientPNCDataForm.value.otherDeliveryPlace).toBeNull(); + expect(component.patientPNCDataForm.value.deliveryType).toBeNull(); + + component.patientPNCDataForm.patchValue({ + deliveryPlace: { deliveryPlace: 'Home-Unsupervised' }, + }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes.length).toBe(1); + + component.patientPNCDataForm.patchValue({ + deliveryPlace: { deliveryPlace: 'Hospital' }, + }); + component.resetOtherPlaceOfDelivery(); + expect(component.selectDeliveryTypes.length).toBe(3); + }); + + it('reset helpers and getters', async () => { + await setup(); + component.patientPNCDataForm.patchValue({ + deliveryComplication: 'dc', + otherDeliveryComplication: 'odc', + postNatalComplication: 'pc', + otherPostNatalComplication: 'opc', + }); + expect(component.deliveryComplication).toBe('dc'); + expect(component.otherDeliveryComplication).toBe('odc'); + expect(component.postNatalComplication).toBe('pc'); + expect(component.otherPostNatalComplication).toBe('opc'); + component.resetOtherDeliveryComplication(); + component.resetOtherPostNatalComplication(); + expect(component.otherDeliveryComplication).toBeNull(); + expect(component.otherPostNatalComplication).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', async () => { + await setup(); + component.ngOnInit(); + const a = spyOn(component.beneficiaryDetailsSubscription, 'unsubscribe'); + const b = spyOn(component.nurseMasterDataSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(a).toHaveBeenCalled(); + expect(b).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/print-page-select/print-page-select.component.spec.ts b/src/app/app-modules/nurse-doctor/print-page-select/print-page-select.component.spec.ts index 67c23360..f9661613 100644 --- a/src/app/app-modules/nurse-doctor/print-page-select/print-page-select.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/print-page-select/print-page-select.component.spec.ts @@ -20,27 +20,84 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, + createHttpServiceMock, +} from 'src/testing/test-utils'; import { PrintPageSelectComponent } from './print-page-select.component'; describe('PrintPageSelectComponent', () => { let component: PrintPageSelectComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const dialogData = { + visitCategory: 'ANC', + printPagePreviewSelect: { + caseSheetANC: false, + caseSheetPNC: true, + caseSheetHistory: false, + caseSheetExamination: true, + caseSheetCovidVaccinationDetails: false, + }, + }; + + const setup = async (data: any) => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS, MatCheckboxModule], declarations: [PrintPageSelectComponent], + providers: [ + ...commonTestProviders(), + { provide: MAT_DIALOG_DATA, useValue: data }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(PrintPageSelectComponent); component = fixture.componentInstance; + }; + + it('defaults every page to selected before init', () => { + const c = new PrintPageSelectComponent( + {} as MatDialogRef, + null, + createHttpServiceMock() as any + ); + expect(Object.values(c.printPagePreviewSelect).every(v => v)).toBeTrue(); + }); + + it('copies visit category and page selection from dialog data and renders', async () => { + await setup(dialogData); fixture.detectChanges(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('ANC'); + expect(component.printPagePreviewSelect).toEqual( + dialogData.printPagePreviewSelect + ); + expect(component.printPagePreviewSelect).not.toBe( + dialogData.printPagePreviewSelect + ); + expect( + fixture.nativeElement.querySelectorAll('mat-checkbox').length + ).toBeGreaterThan(0); + }); + + it('keeps defaults when no dialog data is provided', async () => { + await setup(null); + component.ngOnInit(); + expect(component.visitCategory).toBeUndefined(); + expect(component.printPagePreviewSelect.caseSheetANC).toBeTrue(); + expect(component.printPagePreviewSelect.caseSheetHistory).toBeTrue(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language set', async () => { + await setup(dialogData); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.spec.ts b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.spec.ts index 822f3c5b..c0288538 100644 --- a/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/quick-consult/quick-consult.component.spec.ts @@ -19,28 +19,1191 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, Observable, Subject, of } from 'rxjs'; import { QuickConsultComponent } from './quick-consult.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../shared/services'; +import { TestInVitalsService } from '../shared/services/test-in-vitals.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { IotcomponentComponent } from '../../core/components/iotcomponent/iotcomponent.component'; +import { QuickConsultUtils } from '../shared/utility'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; + +const SERVICE_LINE = JSON.stringify({ vanID: 3, parkingPlaceID: 4 }); + +function masterData() { + return { + chiefComplaintMaster: [ + { chiefComplaintID: 1, chiefComplaint: 'Fever' }, + { chiefComplaintID: 2, chiefComplaint: 'Cough' }, + { chiefComplaintID: 3, chiefComplaint: 'Ache' }, + ], + procedures: [ + { + procedureID: 1, + procedureName: 'RBS Test', + procedureType: 'Laboratory', + }, + { procedureID: 2, procedureName: 'Xray', procedureType: 'Radiology' }, + { procedureID: 3, procedureName: 'CBC', procedureType: 'Laboratory' }, + ], + drugFormMaster: [{ itemFormID: 1, itemFormName: 'Tablet' }], + itemMaster: [ + { + itemID: 1, + itemName: 'Paracetamol', + itemFormID: 1, + strength: '500', + unitOfMeasurement: 'mg', + quantityInHand: 10, + }, + { itemID: 5, itemName: 'Syrup', itemFormID: 2 }, + ], + drugDoseMaster: [ + { itemFormID: 1, dose: '1 tab' }, + { itemFormID: 2, dose: '5 ml' }, + ], + drugFrequencyMaster: ['OD'], + drugDurationUnitMaster: ['Day'], + routeOfAdmin: ['Oral'], + NonEdlMaster: [ + { itemID: 2, itemName: 'Pantop', itemFormID: 1 }, + { itemID: 6, itemName: 'Other', itemFormID: 3 }, + ], + }; +} describe('QuickConsultComponent', () => { let component: QuickConsultComponent; let fixture: ComponentFixture; + let form: FormGroup; + let doctor: any; + let masterSvc: any; + let nurse: any; + let testInVitals: any; + let confirmation: any; + let dialog: any; + let session: any; + let doctorMaster$: BehaviorSubject; + let rbsSelected$: BehaviorSubject; + let rbsCurrent$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + spyOn(console, 'log'); + doctorMaster$ = new BehaviorSubject(null); + rbsSelected$ = new BehaviorSubject(undefined); + rbsCurrent$ = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [QuickConsultComponent], - }).compileComponents(); - })); + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: SERVICE_LINE, + userName: 'doc', + beneficiaryRegID: 11, + visitID: 22, + visitCategory: 'General OPD (QC)', + }, + }), + FormBuilder, + { provide: DoctorService, useValue: autoSpy(DoctorService) }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + doctorMasterData$: doctorMaster$.asObservable(), + }), + }, + { + provide: NurseService, + useValue: autoSpy(NurseService, { + rbsSelectedInInvestigation$: rbsSelected$.asObservable(), + rbsTestResultCurrent$: rbsCurrent$.asObservable(), + }), + }, + { + provide: TestInVitalsService, + useValue: autoSpy(TestInVitalsService), + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(QuickConsultComponent, '') + .compileComponents(); + + doctor = TestBed.inject(DoctorService) as any; + masterSvc = TestBed.inject(MasterdataService) as any; + nurse = TestBed.inject(NurseService) as any; + testInVitals = TestBed.inject(TestInVitalsService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + session = TestBed.inject(SessionStorageService) as any; + + doctor.getGenericVitals.and.returnValue( + of({ + statusCode: 200, + data: { benAnthropometryDetail: null, benPhysicalVitalDetail: null }, + }) + ); + const utils = new QuickConsultUtils(TestBed.inject(FormBuilder), session); + form = utils.createQuickConsultForm(); + }); - beforeEach(() => { + function create(mode = 'new') { fixture = TestBed.createComponent(QuickConsultComponent); component = fixture.componentInstance; + component.patientQuickConsultForm = form; + component.quickConsultMode = mode; fixture.detectChanges(); + // template is overridden, so provide the #prescriptionForm view child + component.prescriptionForm = { form: new FormGroup({}) } as any; + return component; + } + + function drugs(): FormArray { + return (form.controls['prescription'] as FormGroup).controls[ + 'prescribedDrugs' + ] as FormArray; + } + function diagnoses(): FormArray { + return form.controls['provisionalDiagnosisList'] as FormArray; + } + function complaints(): FormArray { + return form.controls['chiefComplaintList'] as FormArray; + } + + describe('lifecycle', () => { + it('initialises state, language and prescription form', () => { + create(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(nurse.clearRbsSelectedInInvestigation).toHaveBeenCalled(); + expect(nurse.clearRbsInVitals).toHaveBeenCalled(); + expect(component.createdBy).toBe('doc'); + expect(component.drugPrescriptionForm).toBe( + form.controls['prescription'] as FormGroup + ); + expect(component.pageLimits).toEqual([0, 5]); + expect(component.drugDurationMaster.length).toBe(29); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + expect(component.rbsTestResultCurrent).toBeNull(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('tracks rbs observables', () => { + create(); + rbsSelected$.next(true); + rbsCurrent$.next(120); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + expect(component.rbsTestResultCurrent).toBe(120); + }); + + it('ngOnChanges resets doctor fetched rbs', () => { + create(); + nurse.rbsTestResultFromDoctorFetch = 5; + component.ngOnChanges(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + }); + + it('ngOnDestroy unsubscribes', () => { + create(); + doctorMaster$.next(masterData()); + nurse.rbsTestResultFromDoctorFetch = 5; + component.ngOnDestroy(); + expect(component.masterDataSubscription.closed).toBeTrue(); + expect(component.getQuickConsultSubscription.closed).toBeTrue(); + expect( + component.rbsSelectedInInvestigationSubscription.closed + ).toBeTrue(); + expect(component.rbsTestResultCurrentSubscription.closed).toBeTrue(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + }); + + it('ngOnDestroy tolerates missing subscriptions', () => { + fixture = TestBed.createComponent(QuickConsultComponent); + component = fixture.componentInstance; + component.ngOnDestroy(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + }); + }); + + describe('loadMasterData', () => { + it('splits masters and loads vitals', () => { + create(); + doctorMaster$.next(masterData()); + expect(component.chiefComplaintTemporarayList[0].length).toBe(3); + expect(component.nonRadiologyMaster.length).toBe(2); + expect(component.radiologyMaster.length).toBe(1); + expect(component.drugMaster.length).toBe(2); + expect(component.edlMaster.length).toBe(2); + expect(doctor.getGenericVitals).toHaveBeenCalledWith({ + benRegID: 11, + benVisitID: 22, + }); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('fetches diagnosis details in view mode', () => { + create('View'); + doctor.getCaseRecordAndReferDetails.and.returnValue(of(null)); + doctorMaster$.next(masterData()); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 11, + 22, + 'General OPD (QC)' + ); + }); + }); + + describe('loadVitalsFromNurse', () => { + const vitals = { + temperature: 98, + systolicBP_1stReading: 120, + diastolicBP_1stReading: 80, + pulseRate: 70, + respiratoryRate: 16, + bloodGlucose_Fasting: 90, + bloodGlucose_Random: 110, + bloodGlucose_2hr_PP: 130, + sPO2: 98, + rbsTestResult: 140, + rbsTestRemarks: 'ok', + }; + + it('patches vitals and disables rbs when fetched', () => { + create(); + doctor.getGenericVitals.and.returnValue( + of({ + data: { + benAnthropometryDetail: { height_cm: 170, weight_Kg: 70, bMI: 24 }, + benPhysicalVitalDetail: vitals, + }, + }) + ); + component.loadVitalsFromNurse(); + expect(component.bMI).toBe(24); + expect(component.temperature).toBe(98); + expect(component.systolicBP_1stReading).toBe(120); + expect(component.diastolicBP_1stReading).toBe(80); + expect(component.pulseRate).toBe(70); + expect(component.respiratoryRate).toBe(16); + expect(component.bloodGlucose_Fasting).toBe(90); + expect(component.bloodGlucose_Random).toBe(110); + expect(component.bloodGlucose_2hr_PP).toBe(130); + expect(component.sPO2).toBe(98); + expect(component.rbsTestResult).toBe(140); + expect(component.rbsTestRemarks).toBe('ok'); + expect(nurse.rbsTestResultFromDoctorFetch).toBe(140); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(140); + expect(form.controls['rbsTestResult'].disabled).toBeTrue(); + expect(testInVitals.setVitalsRBSValueInReports).toHaveBeenCalledWith( + vitals + ); + }); + + it('leaves rbs enabled when no rbs result', () => { + create(); + doctor.getGenericVitals.and.returnValue( + of({ + data: { + benAnthropometryDetail: { height_cm: 1 }, + benPhysicalVitalDetail: { ...vitals, rbsTestResult: null }, + }, + }) + ); + component.loadVitalsFromNurse(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + expect(form.controls['rbsTestResult'].enabled).toBeTrue(); + }); + + it('does nothing when vitals are missing', () => { + create(); + component.loadVitalsFromNurse(); + expect(testInVitals.setVitalsRBSValueInReports).not.toHaveBeenCalled(); + }); + }); + + describe('rbs handling', () => { + beforeEach(() => create()); + + it('checkDiasableRBS reflects investigation and fetched result', () => { + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsSelectedInInvestigation = false; + expect(component.checkDiasableRBS()).toBeFalse(); + component.rbsSelectedInInvestigation = true; + expect(component.checkDiasableRBS()).toBeTrue(); + component.rbsSelectedInInvestigation = false; + nurse.rbsTestResultFromDoctorFetch = 100; + expect(component.checkDiasableRBS()).toBeTrue(); + }); + + it('rbsResultChange enables controls when nothing selected', () => { + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsSelectedInInvestigation = false; + form.patchValue({ rbsTestResult: null }); + expect(component.rbsResultChange()).toBeFalse(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(null); + expect(form.controls['rbsTestRemarks'].enabled).toBeTrue(); + component.rbsSelectedInInvestigation = true; + form.patchValue({ rbsTestResult: 90 }); + expect(component.rbsResultChange()).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(90); + expect(form.controls['rbsTestRemarks'].disabled).toBeTrue(); + }); + + it('checkForRange alerts on out of range values', () => { + form.patchValue({ rbsTestResult: -1 }); + component.checkForRange(); + form.patchValue({ rbsTestResult: 1001 }); + component.checkForRange(); + component.rbsPopup = true; + component.checkForRange(); + form.patchValue({ rbsTestResult: 100 }); + component.rbsPopup = false; + component.checkForRange(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.recheckValue + ); + }); + + it('openIOTRBSModel patches the result from the device dialog', () => { + dialog.open.and.returnValue(createDialogRefMock({ result: 150 })); + component.openIOTRBSModel(); + expect(dialog.open).toHaveBeenCalledWith(IotcomponentComponent, { + width: '600px', + height: '180px', + disableClose: true, + data: { startAPI: environment.startRBSurl }, + }); + expect(component.rbsPopup).toBeFalse(); + expect(form.controls['rbsTestResult'].value).toBe(150); + expect(form.controls['rbsTestResult'].dirty).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(150); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).toHaveBeenCalledWith( + jasmine.objectContaining({ + rbsTestResult: 150, + createdDate: jasmine.any(Date), + }) + ); + }); + + it('openIOTRBSModel handles empty and null results', () => { + dialog.open.and.returnValue(createDialogRefMock({ result: null })); + component.openIOTRBSModel(); + expect(nurse.setRbsInCurrentVitals).not.toHaveBeenCalled(); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).toHaveBeenCalled(); + testInVitals.setVitalsRBSValueInReportsInUpdate.calls.reset(); + dialog.open.and.returnValue(createDialogRefMock(null)); + component.openIOTRBSModel(); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).not.toHaveBeenCalled(); + }); + + it('setRBSResultInReport skips a falsy form value', () => { + component.patientQuickConsultForm = { value: null } as any; + component.setRBSResultInReport(); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).not.toHaveBeenCalled(); + }); + + it('checkTestName toggles rbs investigation', () => { + component.checkTestName({ + value: [{ procedureName: 'CBC' }, { procedureName: 'rbs test' }], + }); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + expect(nurse.setRbsSelectedInInvestigation).toHaveBeenCalledWith(true); + component.checkTestName({ value: [{ procedureName: 'CBC' }] }); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + }); + }); + + describe('form array getters', () => { + it('returns controls or null', () => { + create(); + expect(component.getPrescribedDrugs()).toBe(drugs().controls); + expect(component.getProvisionalDiagnosisList()).toBe( + diagnoses().controls + ); + expect(component.getChiefComplaintList()).toBe(complaints().controls); + component.drugPrescriptionForm = new FormGroup({}); + component.patientQuickConsultForm = new FormGroup({}); + expect(component.getPrescribedDrugs()).toBeNull(); + expect(component.getProvisionalDiagnosisList()).toBeNull(); + expect(component.getChiefComplaintList()).toBeNull(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('diagnosis', () => { + beforeEach(() => create()); + + it('adds diagnosis up to 30', () => { + component.addDiagnosis(); + expect(diagnoses().length).toBe(2); + for (let i = 0; i < 30; i++) component.addDiagnosis(); + expect(diagnoses().length).toBe(30); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.maxDiagnosis + ); + }); + + function validRow() { + return new FormGroup({ + conceptID: new FormControl('1'), + term: new FormControl('t'), + provisionalDiagnosis: new FormControl('t'), + viewProvisionalDiagnosisProvided: new FormControl({ + value: 't', + disabled: true, + }), + }); + } + + it('deletes a valid row after confirmation', () => { + diagnoses().clear(); + diagnoses().push(validRow()); + diagnoses().push(validRow()); + component.deleteDiagnosis(0, diagnoses().at(0)); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn + ); + expect(diagnoses().length).toBe(1); + expect(form.dirty).toBeTrue(); + const last = diagnoses().at(0) as FormGroup; + component.deleteDiagnosis(0, last); + expect(diagnoses().length).toBe(1); + expect(last.value.term).toBeNull(); + expect( + last.controls['viewProvisionalDiagnosisProvided'].enabled + ).toBeTrue(); + }); + + it('keeps a valid row when confirmation is declined', () => { + diagnoses().clear(); + diagnoses().push(validRow()); + confirmation.confirm.and.returnValue(of(false)); + component.deleteDiagnosis(0, diagnoses().at(0)); + expect(diagnoses().at(0).value.term).toBe('t'); + }); + + it('deletes an invalid row without confirmation', () => { + const invalid = () => + new FormGroup({ + conceptID: new FormControl(null, c => ({ req: true })), + viewProvisionalDiagnosisProvided: new FormControl({ + value: 'x', + disabled: true, + }), + }); + diagnoses().clear(); + diagnoses().push(invalid()); + diagnoses().push(invalid()); + component.deleteDiagnosis(1, diagnoses().at(1)); + expect(confirmation.confirm).not.toHaveBeenCalled(); + expect(diagnoses().length).toBe(1); + const row = diagnoses().at(0) as FormGroup; + component.deleteDiagnosis(0, row); + expect( + row.controls['viewProvisionalDiagnosisProvided'].enabled + ).toBeTrue(); + }); + + it('throws for the last default row lacking the view control (app bug)', () => { + expect(() => + component.deleteDiagnosis(0, diagnoses().at(0)) + ).toThrowError(TypeError); + }); + + it('onDiagnosisSelected patches selected concept', () => { + component.onDiagnosisSelected({ conceptID: 'c', term: 'T' }, 0); + expect(diagnoses().at(0).value).toEqual({ + conceptID: 'c', + term: 'T', + provisionalDiagnosis: { conceptID: 'c', term: 'T' }, + }); + component.onDiagnosisSelected(null, 0); + expect(diagnoses().at(0).value.conceptID).toBeNull(); + }); + + it('displayDiagnosis handles strings and objects', () => { + expect(component.displayDiagnosis('abc')).toBe('abc'); + expect(component.displayDiagnosis({ term: 'T' })).toBe('T'); + expect(component.displayDiagnosis(null)).toBe(''); + }); + + it('checkProvisionalDiagnosisValidity', () => { + expect( + component.checkProvisionalDiagnosisValidity({ + value: { conceptID: 1, term: 't' }, + }) + ).toBeFalse(); + expect( + component.checkProvisionalDiagnosisValidity({ value: { term: 't' } }) + ).toBeTrue(); + }); + }); + + describe('getDiagnosisDetails / patchDiagnosisDetails', () => { + beforeEach(() => { + create(); + doctorMaster$.next(masterData()); + }); + + it('patches full diagnosis response', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + findings: { + complaints: [ + { chiefComplaint: 'Fever' }, + { chiefComplaint: 'Zzz' }, + ], + clinicalObservation: 'obs', + }, + investigation: { + laboratoryList: [ + { procedureID: 1, procedureName: 'RBS Test' }, + { procedureID: 2, procedureName: 'Xray' }, + { procedureID: 99, procedureName: 'Unknown' }, + ], + }, + diagnosis: { + diagnosisProvided: 'dx', + externalInvestigation: 'ext', + instruction: 'ins', + prescriptionID: 77, + provisionalDiagnosisList: [ + { term: 'A', conceptID: '1' }, + { term: 'B', conceptID: '2' }, + ], + }, + prescription: [ + { + id: 9, + drugID: 1, + drugName: 'Paracetamol', + drugStrength: '500', + drugUnit: 'mg', + duration: 3, + unit: 'Day', + }, + ], + }, + }) + ); + component.getDiagnosisDetails(11, 22, 'cat'); + expect(component.benChiefComplaints.length).toBe(2); + expect(component.dataSource.data.length).toBe(2); + expect( + component.chiefComplaintMaster.map((c: any) => c.chiefComplaint) + ).toEqual(['Cough', 'Ache']); + expect(component.chiefComplaintTemporarayList[0].length).toBe(2); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + expect(nurse.setRbsSelectedInInvestigation).toHaveBeenCalledWith(true); + expect(form.value.test.map((t: any) => t.procedureID)).toEqual([1]); + expect(form.value.radiology.map((t: any) => t.procedureID)).toEqual([2]); + expect(form.value.clinicalObservation).toBe('obs'); + expect(form.value.diagnosisProvided).toBe('dx'); + expect(form.value.externalInvestigation).toBe('ext'); + expect(form.value.instruction).toBe('ins'); + expect(form.value.prescriptionID).toBe(77); + expect(diagnoses().length).toBe(2); + expect(diagnoses().at(1).get('term')!.value).toBe('B'); + expect( + diagnoses().at(0).get('provisionalDiagnosis')!.disabled + ).toBeTrue(); + expect(drugs().length).toBe(1); + expect(drugs().at(0).value.drugStrength).toBe('500mg'); + expect(drugs().at(0).value.id).toBe(9); + }); + + it('handles sparse responses', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + findings: {}, + investigation: {}, + diagnosis: { provisionalDiagnosisList: [] }, + prescription: [], + }, + }) + ); + component.getDiagnosisDetails(1, 2, 3); + expect(form.value.clinicalObservation).toBeNull(); + expect(drugs().length).toBe(0); + }); + + it('ignores non-200 responses and empty data', () => { + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 500, data: {} }) + ); + component.getDiagnosisDetails(1, 2, 3); + component.patchDiagnosisDetails(null); + expect(component.benChiefComplaints).toEqual([]); + }); + }); + + describe('medicine selection', () => { + beforeEach(() => { + create(); + doctorMaster$.next(masterData()); + component.tempform = { itemFormID: 1, itemFormName: 'Tablet' }; + }); + + it('getFormValueChanged filters drug and dose masters by form', () => { + component.getFormValueChanged(); + expect(component.currentPrescription.formID).toBe(1); + expect(component.currentPrescription.formName).toBe('Tablet'); + expect(component.filteredDrugMaster.map((d: any) => d.itemID)).toEqual([ + 1, 2, + ]); + expect(component.filteredDrugMaster[0].isEDL).toBeTrue(); + expect(component.filteredDrugMaster[1].quantityInHand).toBe(0); + expect(component.filteredDrugDoseMaster.length).toBe(1); + expect(component.subFilteredDrugMaster).toBe( + component.filteredDrugMaster + ); + }); + + it('filterMedicine narrows by prefix', () => { + component.getFormValueChanged(); + component.filterMedicine('PAN'); + expect(component.subFilteredDrugMaster.map((d: any) => d.itemID)).toEqual( + [2] + ); + component.filterMedicine(''); + expect(component.subFilteredDrugMaster.length).toBe(2); + }); + + it('displayFn formats options', () => { + expect( + component.displayFn({ + itemName: 'P', + strength: '500', + unitOfMeasurement: 'mg', + quantityInHand: 4, + }) + ).toBe('P 500mg(4)'); + expect(component.displayFn({ itemName: 'P', strength: '5' })).toBe('P 5'); + expect(component.displayFn(null)).toBe(''); + }); + + it('selectMedicineObject with stock sets primary', () => { + const option = masterData().itemMaster[0]; + component.selectMedicineObject({ + source: { value: { ...option, id: 1, isEDL: true } }, + isUserInput: true, + }); + expect(component.currentPrescription.drugName).toBe('Paracetamol'); + expect(component.isStockAvalable).toBe('primary'); + }); + + it('selectMedicineObject ignores non-user input', () => { + component.selectMedicineObject({ + source: { value: { itemID: 1 } }, + isUserInput: false, + }); + expect(component.currentPrescription.drugID).toBeNull(); + }); + + it('selectMedicineObject without stock asks confirmation', () => { + const option = { + itemID: 2, + itemName: 'Pantop', + strength: '40', + unitOfMeasurement: 'mg', + quantityInHand: 0, + isEDL: false, + }; + component.selectMedicineObject({ + source: { value: option }, + isUserInput: true, + }); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info ' + LANGUAGE_EN.nonEDLMedicine, + LANGUAGE_EN.stockNotAvailableWouldYouPrescribe + 'Pantop 40mg' + ); + expect(component.isStockAvalable).toBe('warn'); + + confirmation.confirm.and.returnValue(of(false)); + component.selectMedicineObject({ + source: { value: { ...option, isEDL: true } }, + isUserInput: true, + }); + expect(confirmation.confirm.calls.mostRecent().args[0]).toBe('info '); + expect(component.tempDrugName).toBeNull(); + expect(component.currentPrescription.drugName).toBe(''); + expect(component.isStockAvalable).toBe(''); + }); + + it('checkNotIssued blocks an already prescribed drug', () => { + drugs().push(new FormGroup({ drugID: new FormControl(1) })); + component.tempDrugName = { itemID: 1 }; + component.currentPrescription.drugID = 1; + component.currentPrescription.drugName = 'Paracetamol'; + expect(component.checkNotIssued(1)).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.medicinePrescribe, + 'info' + ); + expect(component.tempDrugName.itemName).toBe('Paracetamol'); + expect(component.checkNotIssued(5)).toBeTrue(); + }); + + it('reEnterMedicine clears temp name without drug id', () => { + component.tempDrugName = { itemID: 1 }; + component.currentPrescription.drugID = null; + component.reEnterMedicine(); + expect(component.tempDrugName).toBeNull(); + component.reEnterMedicine(); + expect(component.currentPrescription.formID).toBe(1); + }); + + it('submitForUpload adds the medicine and resets', () => { + component.currentPrescription = { + ...component.currentPrescription, + drugID: 1, + drugName: 'Paracetamol', + drugStrength: '500', + drugUnit: null, + duration: 2, + unit: 'Day', + }; + component.submitForUpload(); + expect(drugs().length).toBe(1); + expect(drugs().at(0).value.createdBy).toBe('doc'); + expect(drugs().at(0).value.drugStrength).toBe('500'); + expect(component.tempform).toBeNull(); + expect(component.currentPrescription.drugID).toBeNull(); + }); + + it('deleteMedicine removes from UI or backend', () => { + drugs().push(new FormGroup({ drugID: new FormControl(1) })); + drugs().push(new FormGroup({ drugID: new FormControl(2) })); + component.deleteMedicine(0); + expect(drugs().length).toBe(1); + doctor.deleteMedicine.and.returnValue(of({ statusCode: 200 })); + component.deleteMedicine(0, 5 as any); + expect(doctor.deleteMedicine).toHaveBeenCalledWith(5); + expect(drugs().length).toBe(0); + }); + + it('deleteMedicine keeps rows on backend failure or cancel', () => { + drugs().push(new FormGroup({ drugID: new FormControl(1) })); + doctor.deleteMedicine.and.returnValue(of({ statusCode: 500 })); + component.deleteMedicine(0, 5 as any); + confirmation.confirm.and.returnValue(of(false)); + component.deleteMedicine(0); + expect(drugs().length).toBe(1); + }); + + it('validateDrug and checkDrugFormValidity', () => { + const med = new FormGroup({ drug: new FormControl('x') }); + component.validateDrug('typed', med); + expect(med.value.drug).toBeNull(); + med.patchValue({ drug: { a: 1 } }); + component.validateDrug({ a: 1 }, med); + expect(med.value.drug).toEqual({ a: 1 }); + const full = { + drug: 1, + drugForm: 1, + dose: 1, + frequency: 1, + drugDuration: 1, + drugDurationUnit: 1, + specialInstruction: 1, + }; + expect(component.checkDrugFormValidity({ value: full })).toBeFalse(); + expect( + component.checkDrugFormValidity({ value: { ...full, dose: null } }) + ).toBeTrue(); + }); + + it('display helpers', () => { + expect(component.displayDrugName({ drugDisplayName: 'D' })).toBe('D'); + expect(component.displayDrugName(null)).toBeNull(); + expect(component.displayChiefComplaint({ chiefComplaint: 'C' })).toBe( + 'C' + ); + }); + }); + + describe('chief complaints', () => { + beforeEach(() => { + create(); + doctorMaster$.next(masterData()); + }); + + it('getSnomedCTRecord patches conceptID when found', () => { + masterSvc.getSnomedCTRecord.and.returnValue( + of({ data: { conceptID: 'S1' } }) + ); + component.getSnomedCTRecord('Fever', 0); + expect(complaints().at(0).value.conceptID).toBe('S1'); + masterSvc.getSnomedCTRecord.and.returnValue(of({ data: {} })); + complaints().at(0).patchValue({ conceptID: null }); + component.getSnomedCTRecord('Fever', 0); + expect(complaints().at(0).value.conceptID).toBeNull(); + }); + + it('addChiefComplaint builds a list excluding selected complaints', () => { + complaints().at(0).patchValue({ + chiefComplaint: component.chiefComplaintMaster[0], + }); + component.addChiefComplaint(); + expect(complaints().length).toBe(2); + expect( + component.chiefComplaintTemporarayList[1].map( + (c: any) => c.chiefComplaint + ) + ).toEqual(['Cough', 'Ache']); + }); + + it('addChiefComplaint skips list when all selected', () => { + component.chiefComplaintMaster = []; + component.addChiefComplaint(); + expect(component.chiefComplaintTemporarayList.length).toBe(1); + expect(complaints().length).toBe(2); + }); + + it('filterComplaints moves the selection between lists', () => { + masterSvc.getSnomedCTRecord.and.returnValue(of(null)); + const fever = component.chiefComplaintMaster[0]; + const cough = component.chiefComplaintMaster[1]; + component.chiefComplaintTemporarayList[1] = + component.chiefComplaintMaster.slice(); + component.filterComplaints(fever, 0); + expect(component.selectedChiefComplaintList[0]).toBe(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(fever); + component.filterComplaints(cough, 0); + expect(component.selectedChiefComplaintList[0]).toBe(cough); + expect(component.chiefComplaintTemporarayList[1]).toContain(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(cough); + component.filterComplaints({ chiefComplaint: 'none' }, 0); + expect(component.selectedChiefComplaintList[0]).toBe(cough); + }); + + it('suggestChiefComplaintList filters by string and object', () => { + const fg = (v: any) => + new FormGroup({ chiefComplaint: new FormControl(v) }); + component.suggestChiefComplaintList(fg('co'), 0); + expect(component.suggestedChiefComplaintList[0].length).toBe(1); + component.suggestChiefComplaintList(fg({ chiefComplaint: 'Ach' }), 0); + expect(component.suggestedChiefComplaintList[0][0].chiefComplaint).toBe( + 'Ache' + ); + const none = fg('zzz'); + component.suggestChiefComplaintList(none, 0); + expect(none.value.chiefComplaint).toBeNull(); + complaints().at(0).patchValue({ conceptID: 'x', description: 'd' }); + component.suggestChiefComplaintList(fg(''), 0); + expect(complaints().at(0).value.conceptID).toBeNull(); + expect(complaints().at(0).value.description).toBeNull(); + }); + + it('suggestChiefComplaintList with null temp list throws on length (current behaviour)', () => { + component.chiefComplaintTemporarayList = null; + component.suggestedChiefComplaintList = []; + const fg = new FormGroup({ chiefComplaint: new FormControl('co') }); + expect(() => component.suggestChiefComplaintList(fg, 0)).toThrowError( + TypeError + ); + component.suggestedChiefComplaintList = [[]]; + const fg2 = new FormGroup({ + chiefComplaint: new FormControl({ chiefComplaint: 'x' }), + }); + component.suggestChiefComplaintList(fg2, 0); + expect(fg2.value.chiefComplaint).toBeNull(); + }); + + it('deleteChiefComplaint restores complaint to other lists', () => { + const fever = component.chiefComplaintMaster[0]; + component.chiefComplaintTemporarayList[1] = [ + component.chiefComplaintMaster[1], + ]; + complaints().push( + new FormGroup({ + chiefComplaint: new FormControl(fever), + conceptID: new FormControl(null), + }) + ); + component.selectedChiefComplaintList[1] = fever; + component.suggestedChiefComplaintList[1] = [fever]; + component.deleteChiefComplaint(1, complaints().at(1)); + expect(complaints().length).toBe(1); + expect(component.chiefComplaintTemporarayList[0].length).toBe(4); + expect(component.chiefComplaintTemporarayList[1].length).toBe(1); + expect(component.selectedChiefComplaintList[1]).toBeNull(); + expect(component.suggestedChiefComplaintList[1]).toBeNull(); + expect(form.dirty).toBeTrue(); + }); + + it('deleteChiefComplaint resets the last row and respects cancel', () => { + const row = complaints().at(0); + row.patchValue({ chiefComplaint: 'typed', conceptID: 'c' }); + component.deleteChiefComplaint(0, row); + expect(row.value.conceptID).toBeNull(); + confirmation.confirm.and.returnValue(of(false)); + row.patchValue({ conceptID: 'c' }); + component.deleteChiefComplaint(0, row); + expect(row.value.conceptID).toBe('c'); + }); + + it('deleteChiefComplaintRow removes or resets without confirmation', () => { + const cough = component.chiefComplaintMaster[1]; + const extra = new FormGroup({ + chiefComplaint: new FormControl(cough), + }); + complaints().push(extra); + component.selectedChiefComplaintList[1] = cough; + component.suggestedChiefComplaintList[1] = [cough]; + component.deleteChiefComplaintRow(1, extra); + expect(complaints().length).toBe(1); + expect(component.selectedChiefComplaintList[1]).toBeNull(); + expect( + component.chiefComplaintTemporarayList[0].map( + (c: any) => c.chiefComplaint + ) + ).toEqual(['Ache', 'Cough', 'Cough', 'Fever']); + const row = complaints().at(0) as FormGroup; + row.patchValue({ conceptID: 'c' }); + component.deleteChiefComplaintRow(0, row); + expect(row.value.conceptID).toBeNull(); + }); + + it('sortChiefComplaintList sorts alphabetically', () => { + const list = [ + { chiefComplaint: 'b' }, + { chiefComplaint: 'a' }, + { chiefComplaint: 'b' }, + ]; + component.sortChiefComplaintList(list); + expect(list.map(l => l.chiefComplaint)).toEqual(['a', 'b', 'b']); + }); + + it('checkComplaintFormValidity', () => { + expect(component.checkComplaintFormValidity(null)).toBeFalse(); + expect( + component.checkComplaintFormValidity({ + value: { chiefComplaint: 'a', conceptID: 'b' }, + }) + ).toBeFalse(); + expect( + component.checkComplaintFormValidity({ value: { chiefComplaint: 'a' } }) + ).toBeTrue(); + }); + }); + + describe('canDisable helpers', () => { + beforeEach(() => create()); + + it('canDisableTest for RBS test when a result exists', () => { + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsTestResultCurrent = 100; + expect( + component.canDisableTest({ procedureName: environment.RBSTest }) + ).toBeTrue(); + component.rbsTestResultCurrent = null; + nurse.rbsTestResultFromDoctorFetch = 5; + expect( + component.canDisableTest({ procedureName: environment.RBSTest }) + ).toBeTrue(); + }); + + it('canDisableTest uses the previous lab list', () => { + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsTestResultCurrent = null; + const t1: any = { procedureID: 1, procedureName: 'CBC' }; + expect(component.canDisableTest(t1)).toBeFalse(); + component.previousLabTestList = [{ procedureID: 1 }]; + expect(component.canDisableTest(t1)).toBeTrue(); + expect(t1.disabled).toBeTrue(); + const t2: any = { procedureID: 2, procedureName: 'X' }; + expect(component.canDisableTest(t2)).toBeFalse(); + expect(t2.disabled).toBeFalse(); + }); + + it('canDisableComplaints uses previous complaints', () => { + const c: any = { chiefComplaintID: 1 }; + expect(component.canDisableComplaints(c)).toBeFalse(); + component.previousChiefComplaints = [{ chiefComplaintID: 1 }]; + expect(component.canDisableComplaints(c)).toBeTrue(); + expect(c.disabled).toBeTrue(); + const c2: any = { chiefComplaintID: 2 }; + expect(component.canDisableComplaints(c2)).toBeFalse(); + expect(c2.disabled).toBeFalse(); + }); + }); + + describe('diagnosis search paging', () => { + beforeEach(() => create()); + + it('fetches first page for terms of 3+ chars', () => { + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ id: 1, term: 'fever' }] } }) + ); + component.onDiagnosisInputKeyup(' fev ', 0); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith( + 'fev', + 0 + ); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1, term: 'fever' }, + ]); + expect(component.pageByIndex[0]).toBe(0); + expect(component.loadingMore[0]).toBeFalse(); + component.onDiagnosisInputKeyup('fev', 0); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + }); + + it('resets for short terms', () => { + component.onDiagnosisInputKeyup('fe', 0); + component.onDiagnosisInputKeyup(null as any, 1); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + expect(component.lastQueryByIndex[1]).toBe(''); + expect(masterSvc.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('appends unique results and marks noMore on empty page', () => { + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ id: 1 }, { code: 'c' }] } }) + ); + component.onDiagnosisInputKeyup('fever', 0); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ id: 1 }, { term: 't' }] } }) + ); + component.onAutoNearEnd(0); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledWith( + 'fever', + 1 + ); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { code: 'c' }, + { term: 't' }, + ]); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue(of({})); + component.onAutoNearEnd(0); + expect(component.noMore[0]).toBeTrue(); + masterSvc.searchDiagnosisBasedOnPageNo.calls.reset(); + component.onAutoNearEnd(0); + expect(masterSvc.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('append starts from an empty list', () => { + component.lastQueryByIndex[2] = 'abc'; + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ id: 3 }] } }) + ); + component.onAutoNearEnd(2); + expect(component.suggestedDiagnosisList[2]).toEqual([{ id: 3 }]); + expect(component.pageByIndex[2]).toBe(1); + }); + + it('queues a request while loading and chains on completion', () => { + const pending = new Subject(); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue(pending); + component.onDiagnosisInputKeyup('fever', 0); + expect(component.loadingMore[0]).toBeTrue(); + component.onAutoNearEnd(0); + expect(component.wantMore[0]).toBeTrue(); + component.onDiagnosisInputKeyup('fever', 0); // blocked while loading + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(1); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue( + of({ data: { sctMaster: [{ id: 2 }] } }) + ); + pending.next({ data: { sctMaster: [{ id: 1 }] } }); + pending.complete(); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(2); + expect(component.suggestedDiagnosisList[0]).toEqual([ + { id: 1 }, + { id: 2 }, + ]); + }); + + it('drops stale responses and logs errors', () => { + spyOn(console, 'error'); + const pending = new Subject(); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue(pending); + component.onDiagnosisInputKeyup('fever', 0); + component.lastQueryByIndex[0] = 'other'; + pending.next({ data: { sctMaster: [{ id: 1 }] } }); + expect(component.suggestedDiagnosisList[0]).toEqual([]); + masterSvc.searchDiagnosisBasedOnPageNo.and.returnValue(throwingObs()); + component.loadingMore[1] = false; + component.lastQueryByIndex[1] = 'abc'; + component.onAutoNearEnd(1); + expect(console.error).toHaveBeenCalledWith( + 'Error fetching diagnosis data' + ); + }); + + it('fetchPage without term does nothing', () => { + component.onAutoNearEnd(5); + expect(masterSvc.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); + + it('onPanelReady bootstraps pages until scrollable', () => { + const frames: (() => void)[] = []; + spyOn(window, 'requestAnimationFrame').and.callFake((cb: any) => { + frames.push(cb); + return 0; + }); + const panel: any = { scrollHeight: 100, clientHeight: 100 }; + component.lastQueryByIndex[0] = 'fever'; + let n = 0; + masterSvc.searchDiagnosisBasedOnPageNo.and.callFake(() => + of({ data: { sctMaster: [{ id: ++n }] } }) + ); + component.onPanelReady(0, panel); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(1); + component.loadingMore[0] = true; + frames.shift()!(); + expect(frames.length).toBe(1); // re-queued while loading + component.loadingMore[0] = false; + frames.shift()!(); + frames.shift()!(); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(3); + frames.shift()!(); // max pages reached + expect(frames.length).toBe(0); + expect(masterSvc.searchDiagnosisBasedOnPageNo).toHaveBeenCalledTimes(3); + }); + + it('onPanelReady stops when scrollable, noMore, or short term', () => { + component.lastQueryByIndex[0] = 'fe'; + component.onPanelReady(0, { scrollHeight: 10, clientHeight: 10 } as any); + component.lastQueryByIndex[0] = 'fever'; + component.onPanelReady(0, { scrollHeight: 50, clientHeight: 10 } as any); + component.noMore[0] = true; + component.onPanelReady(0, { scrollHeight: 10, clientHeight: 10 } as any); + expect(masterSvc.searchDiagnosisBasedOnPageNo).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/radiologist-worklist/radiologist-worklist.component.spec.ts b/src/app/app-modules/nurse-doctor/radiologist-worklist/radiologist-worklist.component.spec.ts index d8a16fd8..591619eb 100644 --- a/src/app/app-modules/nurse-doctor/radiologist-worklist/radiologist-worklist.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/radiologist-worklist/radiologist-worklist.component.spec.ts @@ -20,27 +20,240 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; import { RadiologistWorklistComponent } from './radiologist-worklist.component'; +import { DoctorService } from '../shared/services/doctor.service'; +import { CameraService } from '../../core/services/camera.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + autoSpy, + commonTestProviders, + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + throwingObs, +} from 'src/testing/test-utils'; describe('RadiologistWorklistComponent', () => { - let component: RadiologistWorklistComponent; let fixture: ComponentFixture; + let component: RadiologistWorklistComponent; + let doctorService: any; + let cameraService: any; + let benDetails: any; + let confirmation: any; + let session: any; + let router: Router; + let removeSpy: jasmine.Spy; + + const makeList = () => [ + { + beneficiaryID: 11, + benName: 'Kavya', + genderName: 'Female', + age: '30 Years', + VisitCategory: 'Cancer Screening', + districtName: 'Pune', + beneficiaryRegID: 1, + benFlowID: 5, + benVisitID: 6, + visitCode: 7, + doctorFlag: 1, + nurseFlag: 9, + pharmacist_flag: 0, + visitFlowStatusFlag: 'N', + visitDate: '2024-01-02T10:00:00', + }, + { + beneficiaryID: 22, + benName: 'Mohan', + genderName: 'Male', + beneficiaryRegID: 2, + benFlowID: 8, + benVisitID: 9, + visitCode: 10, + VisitCategory: 'NCD screening', + visitFlowStatusFlag: 'D', + }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + doctorService = autoSpy(DoctorService); + doctorService.getRadiologistWorklist.and.returnValue( + of({ statusCode: 200, data: makeList() }) + ); + cameraService = autoSpy(CameraService); + benDetails = autoSpy(BeneficiaryDetailsService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RadiologistWorklistComponent], + providers: [ + ...commonTestProviders(), + { provide: DoctorService, useValue: doctorService }, + { provide: CameraService, useValue: cameraService }, + { provide: BeneficiaryDetailsService, useValue: benDetails }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(RadiologistWorklistComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + removeSpy = spyOn(Storage.prototype, 'removeItem'); + spyOn(console, 'log'); fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('ngOnInit / loadWorklist', () => { + it('sets the role, clears visit data and loads the worklist', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(session.setItem).toHaveBeenCalledWith( + 'currentRole', + 'Radiologist' + ); + expect(removeSpy).toHaveBeenCalledWith('visitCode'); + expect(removeSpy).toHaveBeenCalledWith('caseSheetTMFlag'); + expect(doctorService.getRadiologistWorklist).toHaveBeenCalled(); + expect(component.dataSource.data.map((r: any) => r.sno)).toEqual([1, 2]); + expect(component.filterTerm).toBeNull(); + }); + + it('formats dates and fills missing fields', () => { + const [first, second] = component.beneficiaryList; + expect(first.visitDate).toBe('02-01-2024 10:00 AM '); + expect(second.age).toBe('Not Available'); + expect(second.districtName).toBe('Not Available'); + expect(second.statusMessage).toBe('Not Available'); + expect(second.preferredPhoneNum).toBe('Not Available'); + }); + + it('alerts and clears the table on a failed response', () => { + doctorService.getRadiologistWorklist.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.loadWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(component.dataSource.data).toEqual([]); + }); + + it('alerts on HTTP error', () => { + const err = { status: 0 }; + doctorService.getRadiologistWorklist.and.returnValue(throwingObs(err)); + component.loadWorklist(); + expect(confirmation.alert).toHaveBeenCalledWith(err, 'error'); + }); + }); + + it('ngOnDestroy removes the current role', () => { + component.ngOnDestroy(); + expect(removeSpy).toHaveBeenCalledWith('currentRole'); + }); + + describe('filterBeneficiaryList', () => { + it('restores the full list for an empty term', () => { + component.filteredBeneficiaryList = []; + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + }); + + it('filters on searchable fields and renumbers the table', () => { + component.filterBeneficiaryList('MOHAN'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.dataSource.data[0].beneficiaryID).toBe(22); + expect(component.dataSource.data[0].sno).toBe(1); + }); + + it('returns an empty list when nothing matches', () => { + component.filterBeneficiaryList('xyz-none'); + expect(component.filteredBeneficiaryList).toEqual([]); + expect(component.dataSource.data).toEqual([]); + }); + }); + + it('pageChanged slices the filtered list', () => { + component.filteredBeneficiaryList = [1, 2, 3, 4]; + component.pageChanged({ page: 2, itemsPerPage: 3 }); + expect(component.pagedList as any[]).toEqual([4]); + }); + + describe('patientImageView', () => { + it('views the image when available', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({ benImage: 'img' })); + component.patientImageView(1); + expect(benDetails.getBeneficiaryImage).toHaveBeenCalledWith(1); + expect(cameraService.viewImage).toHaveBeenCalledWith('img'); + }); + + it('alerts when there is no image', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({})); + component.patientImageView(1); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.imageNotFound + ); + }); + }); + + describe('loadDoctorExaminationPage', () => { + it('stores visit details and opens the patient page for a new visit', () => { + const ben = makeList()[0]; + component.loadDoctorExaminationPage(ben); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.confirmtoProceedFurther + ); + expect(session.setItem).toHaveBeenCalledWith('visitCode', 7); + expect(session.setItem).toHaveBeenCalledWith('benFlowID', 5); + expect(session.setItem).toHaveBeenCalledWith('visitID', 6); + expect(session.setItem).toHaveBeenCalledWith('doctorFlag', 1); + expect(session.setItem).toHaveBeenCalledWith('nurseFlag', 9); + expect(session.setItem).toHaveBeenCalledWith('pharmacist_flag', 0); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryRegID', 1); + expect(session.setItem).toHaveBeenCalledWith('beneficiaryID', 11); + expect(session.setItem).toHaveBeenCalledWith( + 'visitCategory', + 'Cancer Screening' + ); + expect(router.navigate).toHaveBeenCalledWith(['/common/patient', 1]); + }); + + it('does not navigate when a new visit is not confirmed', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage(makeList()[0]); + expect(router.navigate).not.toHaveBeenCalled(); + expect(session.setItem).not.toHaveBeenCalledWith('visitID', 6); + }); + + it('opens the case sheet for a completed visit', () => { + component.loadDoctorExaminationPage(makeList()[1]); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.consulation + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetBenFlowID', 8); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategory', + 'NCD screening' + ); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetBeneficiaryRegID', + 2 + ); + expect(session.setItem).toHaveBeenCalledWith('caseSheetVisitID', 9); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + }); + + it('does not open the case sheet when declined', () => { + confirmation.confirm.and.returnValue(of(false)); + component.loadDoctorExaminationPage(makeList()[1]); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/refer/cancer-refer/cancer-refer.component.spec.ts b/src/app/app-modules/nurse-doctor/refer/cancer-refer/cancer-refer.component.spec.ts index 8a51fcc4..721566df 100644 --- a/src/app/app-modules/nurse-doctor/refer/cancer-refer/cancer-refer.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/refer/cancer-refer/cancer-refer.component.spec.ts @@ -20,27 +20,233 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DoctorService, MasterdataService } from '../../shared/services'; import { CancerReferComponent } from './cancer-refer.component'; describe('CancerReferComponent', () => { let component: CancerReferComponent; let fixture: ComponentFixture; + let doctor: any; + let master$: BehaviorSubject; + let session: any; + + const masterData = () => ({ + higherHealthCare: [ + { institutionID: 1, institutionName: 'AIIMS' }, + { institutionID: 2, institutionName: 'KEM' }, + ], + additionalServices: [{ serviceName: 'ICTC' }, { serviceName: 'DOTS' }], + referralReason: 'r', + revisitDate: ['1 week'], + }); + + const makeForm = () => + new FormGroup({ + referredToInstituteID: new FormControl(null), + refrredToAdditionalServiceList: new FormControl(null), + revisitDate: new FormControl(null), + referralReason: new FormControl(null), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup( + mode = '', + sessionSeed: Record = {}, + master: any = null + ) { + doctor = autoSpy(DoctorService); + master$ = new BehaviorSubject(master); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerReferComponent], + providers: [ + ...commonTestProviders({ session: sessionSeed }), + { provide: DoctorService, useValue: doctor }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + doctorMasterData$: master$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(CancerReferComponent); component = fixture.componentInstance; - fixture.detectChanges(); + session = TestBed.inject(SessionStorageService); + component.referForm = makeForm(); + component.referMode = mode; + spyOn(console, 'log'); + component.ngOnInit(); + } + + afterEach(() => component?.ngOnDestroy()); + + it('initialises language, dates and disables referral reason', async () => { + await setup(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.tomorrow.getDate()).toBe( + new Date(Date.now() + 86400000).getDate() + ); + expect(component.maxSchedulerDate).toBeDefined(); + expect(component.referForm.get('referralReason')?.disabled).toBeTrue(); + expect(component.higherHealthcareCenter).toBeUndefined(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('stores master data but does not fetch details outside view mode', async () => { + await setup('', {}, masterData()); + expect(component.higherHealthcareCenter.length).toBe(2); + expect(component.additionalServices.length).toBe(2); + expect(component.referralReason).toBe('r'); + expect(component.revisitDate).toEqual(['1 week']); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('does not fetch refer details in view mode unless doctorFlag is 9', async () => { + await setup('view', { doctorFlag: '2' }, masterData()); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('fetches and patches refer details in view mode when doctorFlag is 9', async () => { + await setup( + 'view', + { + doctorFlag: '9', + beneficiaryRegID: 'b1', + visitID: 'v1', + visitCategory: 'Cancer Screening', + }, + null + ); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + diagnosis: { + revisitDate: '2024-01-01', + referralReason: 'pain', + refrredToAdditionalServiceList: ['ICTC', 'X'], + referredToInstituteID: 2, + }, + }, + }) + ); + master$.next(masterData()); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'b1', + 'v1', + 'Cancer Screening' + ); + const v = component.referForm.getRawValue(); + expect(v.refrredToAdditionalServiceList).toEqual(['ICTC']); + expect(v.referredToInstituteID).toBe(2); + expect(v.revisitDate).toBe('2024-01-01'); + expect(v.referralReason).toBe('pain'); + expect(component.previousServiceList).toEqual(['ICTC', 'X'] as any); + }); + + it('getReferDetails ignores responses without diagnosis', async () => { + await setup('', {}, masterData()); + spyOn(component, 'patchReferDetails'); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getReferDetails(1, 2, 'c'); + doctor.getCaseRecordAndReferDetails.and.returnValue(of(null)); + component.getReferDetails(1, 2, 'c'); + expect(component.patchReferDetails).not.toHaveBeenCalled(); + }); + + it('patchReferDetails handles missing service list and unknown institute', async () => { + await setup('', {}, masterData()); + component.patchReferDetails({ + revisitDate: 'd', + referralReason: 'rr', + referredToInstituteID: 99, + }); + const v = component.referForm.getRawValue(); + expect(v.refrredToAdditionalServiceList).toEqual([]); + expect(v.referredToInstituteID).toBe(99); + expect(v.referralReason).toBe('rr'); + }); + + it('getters expose form controls', async () => { + await setup(); + expect(component.RevisitDate).toBe(component.referForm.get('revisitDate')); + expect(component.ReferralReason).toBe( + component.referForm.get('referralReason') + ); + }); + + it('checkdate patches a timezone-normalised ISO date', async () => { + await setup(); + const d = new Date(2024, 0, 15, 0, 0, 0); + component.checkdate(d); + const expected = new Date( + d.getTime() - d.getTimezoneOffset() * 60000 + ).toISOString(); + expect(component.referForm.value.revisitDate).toBe(expected); + expect(component.tomorrow).toBeDefined(); + }); + + it('canDisable flags services previously referred', async () => { + await setup(); + component.previousServiceList = null as any; + expect(component.canDisable({ serviceName: 'ICTC' })).toBeFalse(); + component.previousServiceList = ['ICTC'] as any; + const s1: any = { serviceName: 'ICTC' }; + const s2: any = { serviceName: 'DOTS' }; + expect(component.canDisable(s1)).toBeTrue(); + expect(s1.disabled).toBeTrue(); + expect(component.canDisable(s2)).toBeFalse(); + expect(s2.disabled).toBeFalse(); + }); + + it('additionalservices toggles the referral reason validator', async () => { + await setup(); + const ctrl = component.referForm.get('referralReason')!; + component.additionalservices(['a']); + expect(component.selectValue).toBe(1); + expect(ctrl.enabled).toBeTrue(); + expect(ctrl.valid).toBeFalse(); + component.additionalservices([]); + expect(ctrl.disabled).toBeTrue(); + component.selectValue = 3; + component.additionalservices(null); + expect(component.selectValue).toBe(3); + expect(ctrl.enabled).toBeTrue(); + }); + + it('higherhealthcarecenter toggles the referral reason validator', async () => { + await setup(); + const ctrl = component.referForm.get('referralReason')!; + component.higherhealthcarecenter(2); + expect(ctrl.enabled).toBeTrue(); + ctrl.setValue('x'); + expect(ctrl.valid).toBeTrue(); + component.higherhealthcarecenter(0); + expect(ctrl.disabled).toBeTrue(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', async () => { + await setup('', {}, masterData()); + component.getReferDetails(1, 2, 3); + const s1 = spyOn(component.doctorMasterDataSubscription, 'unsubscribe'); + const s2 = spyOn(component.referSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(s1).toHaveBeenCalled(); + expect(s2).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/refer/general-refer/general-refer.component.spec.ts b/src/app/app-modules/nurse-doctor/refer/general-refer/general-refer.component.spec.ts index 2ff886f0..d9fe7a67 100644 --- a/src/app/app-modules/nurse-doctor/refer/general-refer/general-refer.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/refer/general-refer/general-refer.component.spec.ts @@ -20,27 +20,302 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { PreviousDetailsComponent } from 'src/app/app-modules/core/components/previous-details/previous-details.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; +import { IdrsscoreService } from '../../shared/services/idrsscore.service'; import { GeneralReferComponent } from './general-refer.component'; describe('GeneralReferComponent', () => { let component: GeneralReferComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let idrs: any; + let confirm: any; + let dialog: any; + let master$: BehaviorSubject; + let suspect$: BehaviorSubject; + let referral$: BehaviorSubject; + let tmc$: BehaviorSubject; + + const masterData = ( + centers: any[] = [{ institutionID: 1, institutionName: 'AIIMS' }] + ) => ({ + higherHealthCare: centers, + additionalServices: [{ serviceName: 'ICTC' }, { serviceName: 'DOTS' }], + revisitDate: ['1 week'], + }); + + const makeForm = () => + new FormGroup({ + referredToInstituteName: new FormControl(null), + refrredToAdditionalServiceList: new FormControl(null), + revisitDate: new FormControl(null), + referralReason: new FormControl(null), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup( + mode = '', + master: any = null, + session: Record = { visitCategory: 'NCD screening' } + ) { + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + master$ = new BehaviorSubject(master); + suspect$ = new BehaviorSubject(0); + referral$ = new BehaviorSubject(0); + tmc$ = new BehaviorSubject(0); + idrs = autoSpy(IdrsscoreService, { + IDRSSuspectedFlag$: suspect$, + tmcSuggestedFlag$: tmc$, + referralSuggestedFlag$: referral$, + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralReferComponent], + providers: [ + ...commonTestProviders({ session }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: IdrsscoreService, useValue: idrs }, + { + provide: MasterdataService, + useValue: autoSpy(MasterdataService, { + doctorMasterData$: master$.asObservable(), + }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(GeneralReferComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + component.referForm = makeForm(); + component.referMode = mode; + spyOn(console, 'log'); + component.ngOnInit(); + } + + afterEach(() => { + component?.ngOnDestroy(); + sessionStorage.removeItem('suspectFlag'); + sessionStorage.removeItem('instFlag'); + }); + + it('initialises language, visit category, dates and flags', async () => { + await setup(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('NCD screening'); + expect(component.showMsg).toBe(0); + expect(sessionStorage.getItem('suspectFlag')).toBe('false'); + expect(component.referForm.get('referralReason')?.disabled).toBeTrue(); + expect(component.maxSchedulerDate).toBeDefined(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('tracks IDRS suspected / referral / tmc flags', async () => { + await setup(); + suspect$.next(2); + expect(component.showMsg).toBe(2); + expect(sessionStorage.getItem('suspectFlag')).toBe('true'); + referral$.next(0); + expect(sessionStorage.getItem('suspectFlag')).toBe('false'); + referral$.next(1); + expect(sessionStorage.getItem('suspectFlag')).toBe('true'); + tmc$.next(1); + expect(component.tmcSuggested).toBe(1); + }); + + it('sets institute flag from master data', async () => { + await setup('', masterData([])); + expect(component.instituteFlag).toBeFalse(); + expect(sessionStorage.getItem('instFlag')).toBe('false'); + master$.next(masterData()); + expect(component.instituteFlag).toBeTrue(); + expect(sessionStorage.getItem('instFlag')).toBe('true'); + expect(component.additionalServices.length).toBe(2); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('in view mode fetches and patches refer details', async () => { + await setup('view', null, { + visitCategory: 'NCD care', + beneficiaryRegID: 'b', + visitID: 'v', + }); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ + statusCode: 200, + data: { + Refer: { + revisitDate: '2024-03-05T10:00:00', + referralReason: 'why', + refrredToAdditionalServiceList: [ + { serviceName: 'ICTC' }, + { serviceName: 'none' }, + ], + referredToInstituteID: 1, + }, + }, + }) + ); + master$.next(masterData()); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'b', + 'v', + 'NCD care' + ); + const v = component.referForm.getRawValue(); + expect(v.revisitDate).toBe('2024-03-05'); + expect(v.referralReason).toBe('why'); + expect(v.refrredToAdditionalServiceList).toEqual([{ serviceName: 'ICTC' }]); + expect(v.referredToInstituteName).toEqual({ + institutionID: 1, + institutionName: 'AIIMS', + }); + expect(component.healthCareReferred).toBeTrue(); + }); + + it('patchReferDetails without services and with unknown institute', async () => { + await setup('', masterData()); + component.patchReferDetails({ + revisitDate: null, + referralReason: null, + referredToInstituteID: 42, + referredToInstituteName: null, + }); + expect( + component.referForm.getRawValue().refrredToAdditionalServiceList + ).toEqual([]); + expect(component.healthCareReferred).toBeFalse(); + }); + + it('getReferDetails ignores empty responses', async () => { + await setup('', masterData()); + spyOn(component, 'patchReferDetails'); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getReferDetails(1, 2, 3); + expect(component.patchReferDetails).not.toHaveBeenCalled(); + }); + + it('getters, checkdate and canDisable', async () => { + await setup(); + expect(component.RevisitDate).toBe(component.referForm.get('revisitDate')); + expect(component.ReferralReason).toBe( + component.referForm.get('referralReason') + ); + const d = new Date(2024, 4, 1); + component.checkdate(d); + expect(component.referForm.value.revisitDate).toBe( + new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString() + ); + expect(component.canDisable({ serviceName: 'a' })).toBeFalse(); + component.previousServiceList = ['a']; + const a: any = { serviceName: 'a' }; + const b: any = { serviceName: 'b' }; + expect(component.canDisable(a)).toBeTrue(); + expect(a.disabled).toBeTrue(); + expect(component.canDisable(b)).toBeFalse(); + expect(b.disabled).toBeFalse(); + }); + + it('additionalservices / higherhealthcarecenter toggle referral reason', async () => { + await setup(); + const ctrl = component.referForm.get('referralReason')!; + component.additionalservices(null); + expect(ctrl.disabled).toBeTrue(); + component.additionalservices([]); + expect(ctrl.disabled).toBeTrue(); + component.higherhealthcarecenter(null); + component.higherhealthcarecenter({}); + expect(ctrl.disabled).toBeTrue(); + component.additionalservices([{}, {}]); + expect(component.selectValueService).toBe(2); + expect(ctrl.enabled).toBeTrue(); + expect(ctrl.valid).toBeFalse(); + component.selectValueService = 0; + component.higherhealthcarecenter({ institutionName: 'AIIMS' }); + expect(component.selectValue).toBe(1); + expect(component.healthCareReferred).toBeTrue(); + expect(ctrl.enabled).toBeTrue(); + }); + + describe('getPreviousReferralHistory', () => { + beforeEach(async () => + setup('', null, { visitCategory: 'ANC', beneficiaryRegID: 'r1' }) + ); + + it('opens dialog when history exists', () => { + const data = { data: [{ a: 1 }] }; + nurse.getPreviousReferredHistory.and.returnValue( + of({ statusCode: 200, data }) + ); + component.getPreviousReferralHistory(); + expect(nurse.getPreviousReferredHistory).toHaveBeenCalledWith( + 'r1', + 'ANC' + ); + expect(dialog.open).toHaveBeenCalledWith(PreviousDetailsComponent, { + data: { + dataList: data, + title: LANGUAGE_EN.previousReferralHistoryDetails, + }, + }); + }); + + it('alerts when no history', () => { + nurse.getPreviousReferredHistory.and.returnValue( + of({ statusCode: 200, data: { data: [] } }) + ); + component.getPreviousReferralHistory(); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.Referdetails.previousReferralhistorynotAvailable + ); + }); + + it('alerts on non-200 and on error', () => { + nurse.getPreviousReferredHistory.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getPreviousReferralHistory(); + nurse.getPreviousReferredHistory.and.returnValue(throwingObs()); + component.getPreviousReferralHistory(); + expect(confirm.alert.calls.allArgs()).toEqual([ + ['Error in fetching previous history', 'error'], + ['Error in fetching previous history', 'error'], + ]); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy clears IDRS flags and unsubscribes', async () => { + await setup('', masterData()); + component.getReferDetails(1, 2, 3); + const u = spyOn(component.referSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(u).toHaveBeenCalled(); + expect(idrs.clearSuspectedArrayFlag).toHaveBeenCalled(); + expect(idrs.clearTMCSuggested).toHaveBeenCalled(); + expect(idrs.clearReferralSuggested).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/refer/refer.component.spec.ts b/src/app/app-modules/nurse-doctor/refer/refer.component.spec.ts index 0a31a076..db036b77 100644 --- a/src/app/app-modules/nurse-doctor/refer/refer.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/refer/refer.component.spec.ts @@ -20,27 +20,43 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { ReferComponent } from './refer.component'; describe('ReferComponent', () => { - let component: ReferComponent; - let fixture: ComponentFixture; + const build = (cat: any) => { + const c = new ReferComponent(); + c.visitCategory = cat; + c.ngOnInit(); + return c; + }; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ReferComponent], - }).compileComponents(); - })); + [ + 'General OPD', + 'ANC', + 'NCD care', + 'PNC', + 'COVID-19 Screening', + 'NCD screening', + 'General OPD (QC)', + ].forEach(cat => + it(`shows general refer for ${cat}`, () => { + const c = build(cat); + expect(c.showGeneralOPD).toBeTrue(); + expect(c.showCancer).toBeFalse(); + }) + ); - beforeEach(() => { - fixture = TestBed.createComponent(ReferComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + it('shows cancer refer for Cancer Screening', () => { + const c = build('Cancer Screening'); + expect(c.showGeneralOPD).toBeFalse(); + expect(c.showCancer).toBeTrue(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('shows nothing for other or missing categories', () => { + for (const cat of ['Other', undefined]) { + const c = build(cat); + expect(c.showGeneralOPD).toBeFalse(); + expect(c.showCancer).toBeFalse(); + } }); }); diff --git a/src/app/app-modules/nurse-doctor/reports/reports.component.spec.ts b/src/app/app-modules/nurse-doctor/reports/reports.component.spec.ts index bf644ca1..c6218849 100644 --- a/src/app/app-modules/nurse-doctor/reports/reports.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/reports/reports.component.spec.ts @@ -20,18 +20,83 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +/// +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { BehaviorSubject, of, throwError } from 'rxjs'; import { ReportsComponent } from './reports.component'; +import { MasterdataService } from '../shared/services'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; describe('ReportsComponent', () => { let component: ReportsComponent; let fixture: ComponentFixture; + let mockMasterdataService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + let mockTrackingService: jasmine.SpyObj; + + const mockLanguageObject = { + noReportsWereMappedforthisService: 'No reports mapped', + noVansWereMappedforthisprovider: 'No vans mapped', + noDataAvailabletoGenerateReport: 'No data available', + }; + + beforeEach(waitForAsync(() => { + mockMasterdataService = jasmine.createSpyObj('MasterdataService', [ + 'getReportsMaster', + 'getVanMaster', + 'getReportData', + ]); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + mockHttpService = jasmine.createSpyObj( + 'HttpServiceService', + ['fetchLanguageSet'], + { + currentLangugae$: new BehaviorSubject( + mockLanguageObject + ).asObservable(), + } + ); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockTrackingService = jasmine.createSpyObj('AmritTrackingService', [ + 'trackFieldInteraction', + ]); + + mockMasterdataService.getReportsMaster.and.returnValue( + of({ statusCode: 200, data: [{ reportID: 1, reportName: 'TestReport' }] }) + ); + mockMasterdataService.getVanMaster.and.returnValue( + of({ statusCode: 200, data: [{ vanID: 1, vehicalNo: 'VAN001' }] }) + ); + mockSessionStorage.getItem.and.returnValue('testProviderServiceID'); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ReportsComponent], - }).compileComponents(); + imports: [ReactiveFormsModule], + providers: [ + FormBuilder, + { provide: MasterdataService, useValue: mockMasterdataService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + { provide: AmritTrackingService, useValue: mockTrackingService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(ReportsComponent, '') + .compileComponents(); })); beforeEach(() => { @@ -43,4 +108,511 @@ describe('ReportsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should set today to a Date', () => { + expect(component.today).toBeInstanceOf(Date); + }); + + it('should create the reportForm', () => { + expect(component.reportForm).toBeDefined(); + expect(component.reportForm.controls['report']).toBeDefined(); + expect(component.reportForm.controls['fromDate']).toBeDefined(); + expect(component.reportForm.controls['toDate']).toBeDefined(); + expect(component.reportForm.controls['van']).toBeDefined(); + }); + + it('should call getReportsMaster', () => { + expect(mockMasterdataService.getReportsMaster).toHaveBeenCalled(); + }); + + it('should call getVanMaster', () => { + expect(mockMasterdataService.getVanMaster).toHaveBeenCalled(); + }); + + it('should call assignSelectedLanguage', () => { + expect(component.currentLanguageSet).toEqual(mockLanguageObject); + }); + }); + + describe('ngDoCheck', () => { + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngDoCheck(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('assignSelectedLanguage', () => { + it('should set currentLanguageSet', () => { + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(mockLanguageObject); + }); + }); + + describe('createReportForm', () => { + it('should return a FormGroup with report, fromDate, toDate, van controls', () => { + const form = component.createReportForm(); + expect(form.controls['report']).toBeDefined(); + expect(form.controls['fromDate']).toBeDefined(); + expect(form.controls['toDate']).toBeDefined(); + expect(form.controls['van']).toBeDefined(); + }); + + it('should have null initial values', () => { + const form = component.createReportForm(); + expect(form.controls['report'].value).toBeNull(); + expect(form.controls['fromDate'].value).toBeNull(); + expect(form.controls['toDate'].value).toBeNull(); + expect(form.controls['van'].value).toBeNull(); + }); + }); + + describe('getReportsMaster', () => { + it('should populate reportMaster on success with data', () => { + mockMasterdataService.getReportsMaster.and.returnValue( + of({ statusCode: 200, data: [{ reportID: 2, reportName: 'Report2' }] }) + ); + component.getReportsMaster(); + expect(component.reportMaster).toEqual([ + { reportID: 2, reportName: 'Report2' }, + ]); + }); + + it('should alert when no reports data', () => { + mockMasterdataService.getReportsMaster.and.returnValue( + of({ statusCode: 200, data: [] }) + ); + component.getReportsMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + mockLanguageObject.noReportsWereMappedforthisService + ); + }); + + it('should alert error when statusCode is not 200', () => { + mockMasterdataService.getReportsMaster.and.returnValue( + of({ statusCode: 500, errorMessage: 'Server error' }) + ); + component.getReportsMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server error', + 'error' + ); + }); + + it('should alert error on HTTP failure', () => { + mockMasterdataService.getReportsMaster.and.returnValue( + throwError(() => 'Network error') + ); + component.getReportsMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + }); + + describe('checkReport', () => { + it('should reset fromDate, toDate, and van to null', () => { + component.reportForm.patchValue({ + report: { reportID: 1, reportName: 'Test' }, + fromDate: new Date(), + toDate: new Date(), + van: { vanID: 1 }, + }); + component.checkReport(); + expect(component.fromDate).toBeNull(); + expect(component.toDate).toBeNull(); + expect(component.van).toBeNull(); + }); + }); + + describe('checkFromDate', () => { + it('should reset toDate and van to null', () => { + const fromDate = new Date(2023, 0, 1); + component.reportForm.patchValue({ + fromDate: fromDate, + toDate: new Date(), + van: { vanID: 1 }, + }); + component.checkFromDate(); + expect(component.toDate).toBeNull(); + expect(component.van).toBeNull(); + }); + + it('should set maxToDate to today when fromDate + 1 month > today', () => { + const recentDate = new Date(); + recentDate.setDate(recentDate.getDate() - 5); + component.today = new Date(); + component.reportForm.patchValue({ fromDate: recentDate }); + component.checkFromDate(); + expect(component.maxToDate.getTime()).toBeLessThanOrEqual( + new Date().getTime() + ); + }); + + it('should set maxToDate to fromDate + 1 month when that is before today', () => { + const oldDate = new Date(2020, 0, 1); + component.today = new Date(); + component.reportForm.patchValue({ fromDate: oldDate }); + component.checkFromDate(); + const expectedMax = new Date(oldDate); + expectedMax.setMonth(oldDate.getMonth() + 1); + expect(component.maxToDate.getTime()).toBe(expectedMax.getTime()); + }); + }); + + describe('checkToDate', () => { + it('should reset van to null', () => { + component.reportForm.patchValue({ van: { vanID: 1 } }); + component.checkToDate(); + expect(component.van).toBeNull(); + }); + }); + + describe('getVanMaster', () => { + it('should populate vanMaster on success with data', () => { + mockMasterdataService.getVanMaster.and.returnValue( + of({ statusCode: 200, data: [{ vanID: 2, vehicalNo: 'VAN002' }] }) + ); + component.getVanMaster(); + expect(component.vanMaster).toEqual([{ vanID: 2, vehicalNo: 'VAN002' }]); + }); + + it('should alert when no vans data', () => { + mockMasterdataService.getVanMaster.and.returnValue( + of({ statusCode: 200, data: [] }) + ); + component.getVanMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + mockLanguageObject.noVansWereMappedforthisprovider + ); + }); + + it('should alert error when statusCode is not 200', () => { + mockMasterdataService.getVanMaster.and.returnValue( + of({ statusCode: 500, errorMessage: 'Van error' }) + ); + component.getVanMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Van error', + 'error' + ); + }); + + it('should alert error on HTTP failure', () => { + mockMasterdataService.getVanMaster.and.returnValue( + throwError(() => 'Van network error') + ); + component.getVanMaster(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Van network error', + 'error' + ); + }); + }); + + describe('form control getters', () => { + it('should return report value', () => { + component.reportForm.patchValue({ report: { reportID: 1 } }); + expect(component.report).toEqual({ reportID: 1 }); + }); + + it('should return fromDate value', () => { + const date = new Date(2023, 5, 1); + component.reportForm.patchValue({ fromDate: date }); + expect(component.fromDate).toEqual(date); + }); + + it('should return toDate value', () => { + const date = new Date(2023, 5, 30); + component.reportForm.patchValue({ toDate: date }); + expect(component.toDate).toEqual(date); + }); + + it('should return van value', () => { + component.reportForm.patchValue({ van: { vanID: 1 } }); + expect(component.van).toEqual({ vanID: 1 }); + }); + }); + + describe('getReportData', () => { + beforeEach(() => { + component.reportForm.patchValue({ + report: { reportID: 1, reportName: 'TestReport' }, + fromDate: new Date(2023, 0, 1), + toDate: new Date(2023, 0, 31), + van: { vanID: 1, vehicalNo: 'VAN001' }, + }); + mockSessionStorage.getItem.and.returnValue('ps1'); + }); + + it('should call masterdataService.getReportData with correct request', () => { + mockMasterdataService.getReportData.and.returnValue( + of({ statusCode: 200, data: [{ col1: 'val1' }] }) + ); + spyOn(component, 'createCriteria'); + component.getReportData(); + + expect(mockMasterdataService.getReportData).toHaveBeenCalledWith( + jasmine.objectContaining({ + reportID: 1, + vanID: 1, + providerServiceMapID: 'ps1', + }) + ); + }); + + it('should set reportData and call createCriteria on success', () => { + const data = [{ col1: 'val1' }]; + mockMasterdataService.getReportData.and.returnValue( + of({ statusCode: 200, data }) + ); + spyOn(component, 'createCriteria'); + component.getReportData(); + + expect(component.reportData).toEqual(data as any); + expect(component.createCriteria).toHaveBeenCalled(); + }); + + it('should alert when no data available', () => { + mockMasterdataService.getReportData.and.returnValue( + of({ statusCode: 200, data: [] }) + ); + component.getReportData(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + mockLanguageObject.noDataAvailabletoGenerateReport + ); + }); + + it('should alert error when statusCode is not 200', () => { + mockMasterdataService.getReportData.and.returnValue( + of({ statusCode: 500, errorMessage: 'Report error' }) + ); + component.getReportData(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Report error', + 'error' + ); + }); + + it('should alert error on HTTP failure', () => { + mockMasterdataService.getReportData.and.returnValue( + throwError(() => 'Report network error') + ); + component.getReportData(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Report network error', + 'error' + ); + }); + }); + + describe('createCriteria', () => { + it('should call exportExcel with criteria array', () => { + component.reportForm.patchValue({ + report: { reportID: 1, reportName: 'TestReport' }, + fromDate: new Date(2023, 0, 1), + toDate: new Date(2023, 0, 31), + van: { vanID: 1, vehicalNo: 'VAN001' }, + }); + spyOn(component, 'exportExcel'); + component.createCriteria(); + + expect(component.exportExcel).toHaveBeenCalled(); + const criteria = (component.exportExcel as jasmine.Spy).calls.mostRecent() + .args[0]; + expect(criteria.length).toBe(4); + expect(criteria[0].Filter_Name).toBe('Start_Date'); + expect(criteria[1].Filter_Name).toBe('End_Date'); + expect(criteria[2].Filter_Name).toBe('Vehicle'); + expect(criteria[2].value).toBe('VAN001'); + expect(criteria[3].Filter_Name).toBe('Report'); + expect(criteria[3].value).toBe('TestReport'); + }); + }); + + describe('exportExcel', () => { + it('should reset reportForm after execution', () => { + component.reportData = []; + const criteria = [{ Filter_Name: 'Start_Date', value: new Date() }]; + component.exportExcel(criteria); + expect(component.reportForm.controls['report'].value).toBeNull(); + }); + + it('should set criteriaHead from criteria array', () => { + component.reportData = []; + const criteria = [{ Filter_Name: 'Start_Date', value: '2023-01-01' }]; + component.exportExcel(criteria); + expect(component.criteriaHead).toEqual(['Filter_Name', 'value']); + }); + + it('should alert No Record Found when reportData array is filtered to empty', () => { + component.reportData = [] as any; + component.reportForm.patchValue({ + report: { reportID: 1, reportName: 'TestReport' }, + }); + const criteria = [{ Filter_Name: 'Start_Date', value: '2023-01-01' }]; + component.exportExcel(criteria); + // reportData.length is 0, so the "if (this.reportData.length > 0)" branch is not entered + // and the form is reset + expect(component.reportForm.controls['report'].value).toBeNull(); + }); + + it('should handle empty criteria array', () => { + component.reportData = []; + component.exportExcel([]); + expect(component.reportForm.controls['report'].value).toBeNull(); + }); + + it('should replace null values in criteria with empty string', () => { + component.reportData = []; + const criteria: any[] = [{ Filter_Name: 'Start_Date', value: null }]; + component.exportExcel(criteria); + expect(criteria[0].value).toBe(''); + }); + }); + + describe('exportExcel with report data', () => { + let anchorDispatch: jasmine.Spy; + beforeEach(() => { + spyOn(console, 'log'); + spyOn(URL, 'createObjectURL').and.returnValue('blob:mock'); + spyOn(URL, 'revokeObjectURL'); + anchorDispatch = spyOn( + HTMLAnchorElement.prototype, + 'dispatchEvent' + ).and.returnValue(true); + component.reportForm.patchValue({ + report: { reportID: 1, reportName: 'My Report' }, + }); + (component as any).reportData = [ + { name: 'A', age: null }, + { name: 'B', age: 4 }, + ]; + }); + + afterEach(() => { + delete (navigator as any).msSaveBlob; + }); + + it('builds the workbook, downloads it via a hidden link and alerts success', async () => { + let done!: () => void; + const clicked = new Promise(r => (done = r)); + const click = spyOn(HTMLAnchorElement.prototype, 'click').and.callFake( + function (this: HTMLAnchorElement) { + expect(this.download).toBe('My_Report.xlsx'); + done(); + } + ); + const criteria: any[] = [{ Filter_Name: 'Van', value: null }]; + component.exportExcel(criteria); + expect((component as any).reportData[0].age).toBe(''); + expect(component.criteriaHead).toEqual(['Filter_Name', 'value']); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Report Downloaded', + 'success' + ); + expect(component.reportForm.controls['report'].value).toBeNull(); + await clicked; + await new Promise(r => setTimeout(r, 10)); + expect(click).toHaveBeenCalledTimes(1); + expect(URL.createObjectURL).toHaveBeenCalled(); + // file-saver dispatches its own synthetic click on a separate anchor + expect(anchorDispatch).toHaveBeenCalled(); + }); + + it('uses navigator.msSaveBlob when available', async () => { + let done!: () => void; + const saved = new Promise(r => (done = r)); + const msSaveBlob = jasmine + .createSpy('msSaveBlob') + .and.callFake(() => done()); + (navigator as any).msSaveBlob = msSaveBlob; + const click = spyOn(HTMLAnchorElement.prototype, 'click'); + component.exportExcel([]); + await saved; + await new Promise(r => setTimeout(r, 10)); + expect(msSaveBlob).toHaveBeenCalledWith(jasmine.any(Blob), 'My Report'); + expect(click).not.toHaveBeenCalled(); + }); + }); + + describe('manipulateNullReportData', () => { + it('should replace null values with empty string', () => { + const data = [ + { name: 'Test', age: null, city: 'Delhi' }, + { name: null, age: 25, city: null }, + ]; + const result = component.manipulateNullReportData(data); + expect(result[0].age).toBe(''); + expect(result[1].name).toBe(''); + expect(result[1].city).toBe(''); + }); + + it('should not modify non-null values', () => { + const data = [{ name: 'Test', age: 30 }]; + const result = component.manipulateNullReportData(data); + expect(result[0].name).toBe('Test'); + expect(result[0].age).toBe(30); + }); + + it('should return empty array for empty input', () => { + const result = component.manipulateNullReportData([]); + expect(result).toEqual([]); + }); + }); + + describe('modifyHeader', () => { + it('should convert camelCase to Title Case', () => { + const headers = ['firstName']; + const result = component.modifyHeader(headers, 65); + expect(result).toBe('First Name'); + }); + + it('should replace I D with ID', () => { + const headers = ['beneficiaryID']; + const result = component.modifyHeader(headers, 65); + expect(result).toBe('Beneficiary ID'); + }); + + it('should handle already proper case', () => { + const headers = ['Name']; + const result = component.modifyHeader(headers, 65); + expect(result).toBe('Name'); + }); + + it('should handle multiple uppercase letters', () => { + const headers = ['providerServiceMapID']; + const result = component.modifyHeader(headers, 65); + expect(result).toContain('ID'); + }); + }); + + describe('manipulateSheetCellsAndColumns', () => { + it('should modify worksheet header cells', () => { + const head = ['firstName', 'lastName']; + const mockWorksheet: any = { + A1: { v: 'firstName', w: 'firstName' }, + B1: { v: 'lastName', w: 'lastName' }, + }; + const result = component.manipulateSheetCellsAndColumns( + head, + mockWorksheet + ); + expect(result['A1'].v).toBe('First Name'); + expect(result['A1'].w).toBeUndefined(); + expect(result['B1'].v).toBe('Last Name'); + }); + }); + + describe('trackFieldInteraction', () => { + it('should call trackingService.trackFieldInteraction with field name and form context', () => { + component.trackFieldInteraction('reportType'); + expect(mockTrackingService.trackFieldInteraction).toHaveBeenCalledWith( + 'reportType', + 'Reports' + ); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/shared/services/audio-recording.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/audio-recording.service.spec.ts new file mode 100644 index 00000000..c0c46c93 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/services/audio-recording.service.spec.ts @@ -0,0 +1,259 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { AudioRecordingService } from './audio-recording.service'; +import { environment } from 'src/environments/environment'; + +describe('AudioRecordingService', () => { + let service: AudioRecordingService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [AudioRecordingService], + }); + + service = TestBed.inject(AudioRecordingService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('getRecordedBlob', () => { + it('should return an Observable', () => { + const obs = service.getRecordedBlob(); + expect(obs.subscribe).toBeDefined(); + }); + }); + + describe('getRecordedTime', () => { + it('should return an Observable', () => { + const obs = service.getRecordedTime(); + expect(obs.subscribe).toBeDefined(); + }); + }); + + describe('recordingFailed', () => { + it('should return an Observable', () => { + const obs = service.recordingFailed(); + expect(obs.subscribe).toBeDefined(); + }); + }); + + describe('startRecording', () => { + it('should return early if recorder already exists', () => { + (service as any).recorder = {}; + + const getUserMediaSpy = spyOn(navigator.mediaDevices, 'getUserMedia'); + + service.startRecording(); + + expect(getUserMediaSpy).not.toHaveBeenCalled(); + }); + + it('should call getUserMedia, start recording and tick the timer', async () => { + (service as any).recorder = null; + const ctx = new AudioContext(); + const stream = ctx.createMediaStreamDestination().stream; + let resolveStarted!: () => void; + const started = new Promise(r => (resolveStarted = r)); + const realRecord = (service as any).record.bind(service); + spyOn(service as any, 'record').and.callFake(() => { + realRecord(); + resolveStarted(); + }); + const getUserMediaSpy = spyOn( + navigator.mediaDevices, + 'getUserMedia' + ).and.returnValue(Promise.resolve(stream)); + let intervalFn: any; + spyOn(window, 'setInterval').and.callFake(((fn: any) => { + intervalFn = fn; + return 4242 as any; + }) as any); + const clear = spyOn(window, 'clearInterval'); + const times: string[] = []; + service.getRecordedTime().subscribe(t => times.push(t)); + try { + service.startRecording(); + expect(getUserMediaSpy).toHaveBeenCalledWith({ audio: true }); + await started; + expect((service as any).stream).toBe(stream); + expect((service as any).recorder).toBeTruthy(); + expect((service as any).startTime).toBeTruthy(); + intervalFn(); + expect(times[0]).toBe('00:00'); + expect(times[1]).toBe('00:00'); + service.abortRecording(); + expect(clear).toHaveBeenCalledWith(4242); + expect((service as any).recorder).toBeNull(); + expect((service as any).stream).toBeNull(); + } finally { + await ctx.close(); + } + }); + + it('should emit recording failed when getUserMedia rejects', done => { + (service as any).recorder = null; + + spyOn(navigator.mediaDevices, 'getUserMedia').and.returnValue( + Promise.reject(new Error('Permission denied')) + ); + + service.recordingFailed().subscribe(msg => { + expect(msg).toBe('Error in recording'); + done(); + }); + + service.startRecording(); + }); + }); + + describe('abortRecording', () => { + it('should call stopMedia', () => { + const stopMediaSpy = spyOn(service as any, 'stopMedia'); + + service.abortRecording(); + + expect(stopMediaSpy).toHaveBeenCalled(); + }); + }); + + describe('stopRecording', () => { + it('should not throw when recorder is null', () => { + (service as any).recorder = null; + + expect(() => service.stopRecording()).not.toThrow(); + }); + + it('should call recorder.stop when recorder exists', () => { + const stopSpy = jasmine.createSpy('stop'); + (service as any).recorder = { stop: stopSpy }; + + service.stopRecording(); + + expect(stopSpy).toHaveBeenCalled(); + }); + }); + + describe('stopRecording callbacks', () => { + it('emits the recorded blob and stops media on success', () => { + const blob = new Blob(['a']); + const stopMedia = spyOn(service as any, 'stopMedia').and.callThrough(); + (service as any).startTime = new Date(); + (service as any).recorder = { + stop: (ok: any) => ok(blob), + }; + const out: any[] = []; + service.getRecordedBlob().subscribe(v => out.push(v)); + service.stopRecording(); + expect(stopMedia).toHaveBeenCalled(); + expect(out.length).toBe(1); + expect(out[0].blob).toBe(blob); + expect(out[0].title).toMatch(/^audio_\d+\.wav$/); + }); + + it('does not emit when there is no start time', () => { + (service as any).startTime = null; + (service as any).recorder = { stop: (ok: any) => ok(new Blob()) }; + const out: any[] = []; + service.getRecordedBlob().subscribe(v => out.push(v)); + service.stopRecording(); + expect(out).toEqual([]); + }); + + it('reports failure and stops media on error', () => { + (service as any).recorder = { + stop: (_ok: any, fail: any) => fail(), + }; + const msgs: string[] = []; + service.recordingFailed().subscribe(m => msgs.push(m)); + service.stopRecording(); + expect(msgs).toEqual(['Recording Failed']); + expect((service as any).recorder).toBeNull(); + }); + }); + + describe('toString (private)', () => { + it('pads single digits and zero', () => { + expect((service as any).toString(0)).toBe('00'); + expect((service as any).toString(5)).toBe('05'); + expect((service as any).toString(12)).toBe(12); + }); + }); + + describe('stopMedia (private)', () => { + it('should clean up recorder and stream when recorder exists', () => { + const trackStopSpy = jasmine.createSpy('trackStop'); + (service as any).recorder = {}; + (service as any).interval = 1; + (service as any).startTime = new Date(); + (service as any).stream = { + getAudioTracks: () => [{ stop: trackStopSpy }], + }; + + (service as any).stopMedia(); + + expect((service as any).recorder).toBeNull(); + expect((service as any).startTime).toBeNull(); + expect((service as any).stream).toBeNull(); + expect(trackStopSpy).toHaveBeenCalled(); + }); + + it('should do nothing when recorder is null', () => { + (service as any).recorder = null; + (service as any).stream = { getAudioTracks: () => [] }; + + (service as any).stopMedia(); + + expect((service as any).stream).not.toBeNull(); + }); + }); + + describe('getResultStatus', () => { + it('should make POST request with FormData', () => { + const formData = new FormData(); + formData.append('file', new Blob(['test']), 'test.wav'); + + service.getResultStatus(formData).subscribe((res: any) => { + expect(res).toEqual({ status: 'success' }); + }); + + const req = httpMock.expectOne(environment.getResultStatusURL); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toBe(formData); + req.flush({ status: 'success' }); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/services/doctor.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/doctor.service.spec.ts new file mode 100644 index 00000000..eb481933 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/services/doctor.service.spec.ts @@ -0,0 +1,2614 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { FormControl, FormGroup, FormArray } from '@angular/forms'; +import { DoctorService } from './doctor.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; + +describe('DoctorService', () => { + let service: DoctorService; + let httpMock: HttpTestingController; + let sessionStorageSpy: jasmine.SpyObj; + + const mockSessionStore: Record = { + providerServiceID: '1234', + serviceID: '5', + serviceLineDetails: JSON.stringify({ + vanID: 10, + parkingPlaceID: 20, + facilityID: 30, + }), + beneficiaryRegID: '9876', + visitID: '111', + benFlowID: '222', + beneficiaryID: '333', + doctorFlag: '1', + nurseFlag: '2', + pharmacist_flag: '3', + visitCode: 'VC001', + sessionID: 'sess1', + userName: 'testUser', + visitCategory: 'General OPD', + beneficiaryData: JSON.stringify({ + doctorFlag: '1', + nurseFlag: '2', + pharmacist_flag: 'Not Available', + benVisitID: '111', + beneficiaryID: '333', + benFlowID: '222', + vanID: 10, + }), + }; + + beforeEach(() => { + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + sessionStorageSpy.getItem.and.callFake( + (key: string) => mockSessionStore[key] || null + ); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + DoctorService, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + ], + }); + + service = TestBed.inject(DoctorService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('Properties initialization', () => { + it('should initialize enableCovidVaccinationButton to false', () => { + expect(service.enableCovidVaccinationButton).toBeFalse(); + }); + + it('should initialize enableButton to false', () => { + expect(service.enableButton).toBeFalse(); + }); + + it('should initialize historyResponse to empty array', () => { + expect(service.historyResponse).toEqual([]); + }); + }); + + describe('BehaviorSubjects', () => { + it('should emit initial value for enableVitalsUpdateButton$', done => { + service.enableVitalsUpdateButton$.subscribe(val => { + expect(val).toBeFalse(); + done(); + }); + }); + + it('should emit updated value via setValueToEnableVitalsUpdateButton', done => { + service.setValueToEnableVitalsUpdateButton(true); + service.enableVitalsUpdateButton$.subscribe(val => { + expect(val).toBeTrue(); + done(); + }); + }); + + it('should emit initial value for populateHistoryResponse$', done => { + service.populateHistoryResponse$.subscribe(val => { + expect(val).toEqual([]); + done(); + }); + }); + + it('should emit data via setCapturedHistoryByNurse', done => { + const mockHistory = [{ key: 'value' }]; + service.setCapturedHistoryByNurse(mockHistory); + service.populateHistoryResponse$.subscribe(val => { + expect(val).toEqual(mockHistory); + done(); + }); + }); + }); + + describe('clearCache', () => { + it('should set generalHistory, getVisitComplaint, caseRecordAndReferDetails to null', () => { + service.generalHistory = 'something'; + service.getVisitComplaint = 'something'; + service.caseRecordAndReferDetails = 'something'; + service.clearCache(); + expect(service.generalHistory).toBeNull(); + expect(service.getVisitComplaint).toBeNull(); + expect(service.caseRecordAndReferDetails).toBeNull(); + }); + }); + + describe('Worklist methods', () => { + it('getDoctorWorklist should GET with correct URL', () => { + service.getDoctorWorklist().subscribe(); + const req = httpMock.expectOne(environment.doctorWorkList + '1234/5/10'); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + + it('getSpecialistWorklist should GET with correct URL', () => { + service.getSpecialistWorklist().subscribe(); + const req = httpMock.expectOne( + environment.specialistWorkListURL + '1234/5' + ); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + + it('getDoctorFutureWorklist should GET with correct URL', () => { + service.getDoctorFutureWorklist().subscribe(); + const req = httpMock.expectOne( + environment.doctorFutureWorkList + '1234/5' + ); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + + it('getSpecialistFutureWorklist should GET with correct URL', () => { + service.getSpecialistFutureWorklist().subscribe(); + const req = httpMock.expectOne( + environment.specialistFutureWorkListURL + '1234/5' + ); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + + it('getRadiologistWorklist should GET with correct URL', () => { + service.getRadiologistWorklist().subscribe(); + const req = httpMock.expectOne( + environment.radiologistWorklist + '1234/5/10' + ); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + + it('getOncologistWorklist should GET with correct URL', () => { + service.getOncologistWorklist().subscribe(); + const req = httpMock.expectOne( + environment.oncologistWorklist + '1234/5/10' + ); + expect(req.request.method).toBe('GET'); + req.flush({ data: [] }); + }); + }); + + describe('Simple POST/GET methods', () => { + it('getServiceOnState should POST to getServiceOnStateUrl', () => { + service.getServiceOnState().subscribe(); + const req = httpMock.expectOne(environment.getServiceOnStateUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush({ data: [] }); + }); + + it('updateBeneficiaryArrivalStatus should POST arrival details', () => { + const details = { benFlowID: '1', arrivalStatus: true }; + service.updateBeneficiaryArrivalStatus(details).subscribe(); + const req = httpMock.expectOne( + environment.updateBeneficiaryArrivalStatusUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(details); + req.flush({ statusCode: 200 }); + }); + + it('cancelBeneficiaryTCRequest should POST tcRequest', () => { + const tcReq = { benFlowID: '1' }; + service.cancelBeneficiaryTCRequest(tcReq).subscribe(); + const req = httpMock.expectOne(environment.cancelBeneficiaryTCRequestUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(tcReq); + req.flush({ statusCode: 200 }); + }); + + it('confirmStatus should POST with benVisitID', () => { + service.confirmStatus('VID1').subscribe(); + const req = httpMock.expectOne(environment.updateVisitStatus); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benVisitID: 'VID1' }); + req.flush({ statusCode: 200 }); + }); + + it('getMMUHistory should POST beneficiaryRegID from session', () => { + service.getMMUHistory().subscribe(); + const req = httpMock.expectOne(environment.previousMMUHistoryUrl); + expect(req.request.body).toEqual({ beneficiaryRegID: '9876' }); + req.flush({ data: [] }); + }); + + it('getTMHistory should POST beneficiaryRegID from session', () => { + service.getTMHistory().subscribe(); + const req = httpMock.expectOne(environment.previousTMHistoryUrl); + expect(req.request.body).toEqual({ beneficiaryRegID: '9876' }); + req.flush({ data: [] }); + }); + + it('getMCTSHistory should POST beneficiaryRegID from session', () => { + service.getMCTSHistory().subscribe(); + const req = httpMock.expectOne(environment.previousMCTSHistoryUrl); + expect(req.request.body).toEqual({ beneficiaryRegID: '9876' }); + req.flush({ data: [] }); + }); + + it('get104History should POST beneficiaryRegID from session', () => { + service.get104History().subscribe(); + const req = httpMock.expectOne(environment.previous104HistoryUrl); + expect(req.request.body).toEqual({ beneficiaryRegID: '9876' }); + req.flush({ data: [] }); + }); + + it('deleteMedicine should POST with id', () => { + service.deleteMedicine(42).subscribe(); + const req = httpMock.expectOne(environment.drugDeleteUrl); + expect(req.request.body).toEqual({ id: 42 }); + req.flush({ statusCode: 200 }); + }); + + it('getMMUCasesheetData should POST caseSheetRequest', () => { + const csReq = { benRegID: '1', benVisitID: '2' }; + service.getMMUCasesheetData(csReq).subscribe(); + const req = httpMock.expectOne(environment.getMMUCasesheetDataUrl); + expect(req.request.body).toEqual(csReq); + req.flush({ data: {} }); + }); + + it('getTMCasesheetData should POST caseSheetRequest', () => { + const csReq = { benRegID: '1' }; + service.getTMCasesheetData(csReq).subscribe(); + const req = httpMock.expectOne(environment.getTMCasesheetDataUrl); + expect(req.request.body).toEqual(csReq); + req.flush({ data: {} }); + }); + + it('getArchivedReports should POST', () => { + const ar = { benRegID: '1' }; + service.getArchivedReports(ar).subscribe(); + const req = httpMock.expectOne(environment.archivedReportsUrl); + expect(req.request.body).toEqual(ar); + req.flush({ data: [] }); + }); + + it('getPatientMCTSCallHistory should POST', () => { + const callId = { callDetailID: '10' }; + service.getPatientMCTSCallHistory(callId).subscribe(); + const req = httpMock.expectOne(environment.patientMCTSCallHistoryUrl); + expect(req.request.body).toEqual(callId); + req.flush({ data: [] }); + }); + + it('getMasterSpecialization should POST empty body', () => { + service.getMasterSpecialization().subscribe(); + const req = httpMock.expectOne(environment.getMasterSpecializationUrl); + expect(req.request.body).toEqual({}); + req.flush({ data: [] }); + }); + + it('getSpecialist should POST specialistReqObj', () => { + const obj = { specializationID: 1 }; + service.getSpecialist(obj).subscribe(); + const req = httpMock.expectOne(environment.getSpecialistUrl); + expect(req.request.body).toEqual(obj); + req.flush({ data: [] }); + }); + + it('getAvailableSlot should POST', () => { + const obj = { date: '2024-01-01' }; + service.getAvailableSlot(obj).subscribe(); + const req = httpMock.expectOne(environment.getAvailableSlotUrl); + expect(req.request.body).toEqual(obj); + req.flush({ data: [] }); + }); + + it('scheduleTC should POST schedulerRequest', () => { + const obj = { benRegID: '1' }; + service.scheduleTC(obj).subscribe(); + const req = httpMock.expectOne(environment.scheduleTCUrl); + expect(req.request.body).toEqual(obj); + req.flush({ statusCode: 200 }); + }); + + it('beneficiaryTCRequestStatus should POST', () => { + const obj = { benFlowID: '1' }; + service.beneficiaryTCRequestStatus(obj).subscribe(); + const req = httpMock.expectOne(environment.beneficiaryTCRequestStatusUrl); + expect(req.request.body).toEqual(obj); + req.flush({ statusCode: 200 }); + }); + + it('getSwymedMail should GET with vanID', () => { + service.getSwymedMail().subscribe(); + const req = httpMock.expectOne(environment.getSwymedMailUrl + '/10'); + expect(req.request.method).toBe('GET'); + req.flush({ data: {} }); + }); + + it('downloadSign should GET with blob responseType', () => { + service.downloadSign(42).subscribe(); + const req = httpMock.expectOne(environment.downloadSignUrl + 42); + expect(req.request.method).toBe('GET'); + expect(req.request.responseType).toBe('blob'); + req.flush(new Blob()); + }); + + it('getUserId should GET with userName', () => { + service.getUserId('testUser').subscribe(); + const req = httpMock.expectOne(environment.getUserId + 'testUser'); + expect(req.request.method).toBe('GET'); + req.flush({ data: { userID: 1 } }); + }); + + it('getAssessment should GET with benRegID', () => { + service.getAssessment('REG1').subscribe(); + const req = httpMock.expectOne(environment.getAssessmentIdUrl + '/REG1'); + expect(req.request.method).toBe('GET'); + req.flush({ data: {} }); + }); + + it('getAssessmentDet should GET with assessmentId', () => { + service.getAssessmentDet('A1').subscribe(); + const req = httpMock.expectOne(environment.getAssessmentUrl + '/A1'); + expect(req.request.method).toBe('GET'); + req.flush({ data: {} }); + }); + + it('checkUsersignatureExist should GET with userID', () => { + service.checkUsersignatureExist(5).subscribe(); + const req = httpMock.expectOne(environment.checkUsersignExistUrl + 5); + expect(req.request.method).toBe('GET'); + req.flush({ data: true }); + }); + + it('getPreviousVisitAnthropometry should POST', () => { + const obj = { benRegID: '1' }; + service.getPreviousVisitAnthropometry(obj).subscribe(); + const req = httpMock.expectOne(environment.getPreviousAnthropometryUrl); + expect(req.request.body).toEqual(obj); + req.flush({ data: [] }); + }); + + it('getPreviousSignificiantFindings should POST', () => { + const obj = { benRegID: '1' }; + service.getPreviousSignificiantFindings(obj).subscribe(); + const req = httpMock.expectOne( + environment.getPreviousSignificiantFindingUrl + ); + expect(req.request.body).toEqual(obj); + req.flush({ data: [] }); + }); + + it('getHRPDetails should POST and cache result', () => { + service.getHRPDetails('REG1', 'VC1').subscribe(); + const req = httpMock.expectOne(environment.loadHRPUrl); + expect(req.request.body).toEqual({ + benRegID: 'REG1', + visitCode: 'VC1', + }); + req.flush({ data: {} }); + expect(service.HRPDetails).toBeDefined(); + }); + }); + + describe('Cancer screening methods', () => { + it('getVisitDetails should POST with correct body', () => { + service.getVisitDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningVisitDetails + ); + expect(req.request.body).toEqual({ + benRegID: 'BEN1', + benVisitID: 'VIS1', + visitCode: 'VC001', + }); + req.flush({ data: {} }); + }); + + it('getCancerHistoryDetails should POST', () => { + service.getCancerHistoryDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningHistoryDetails + ); + expect(req.request.body.benRegID).toBe('BEN1'); + req.flush({ data: {} }); + }); + + it('getCancerVitalsDetails should POST', () => { + service.getCancerVitalsDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningVitalsDetails + ); + expect(req.request.body.benRegID).toBe('BEN1'); + req.flush({ data: {} }); + }); + + it('getCancerExaminationDetails should POST', () => { + service.getCancerExaminationDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningExaminationDetails + ); + expect(req.request.body.benRegID).toBe('BEN1'); + req.flush({ data: {} }); + }); + }); + + describe('NCD Screening methods', () => { + it('getNcdScreeningDetails should POST', () => { + service.getNcdScreeningDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getNCDScreeningDetails); + expect(req.request.body).toEqual({ + benRegID: 'BEN1', + benVisitID: 'VIS1', + visitCode: 'VC001', + }); + req.flush({ data: {} }); + }); + }); + + describe('ANC methods', () => { + it('getAncCareDetails should POST', () => { + service.getAncCareDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getANCDetailsUrl); + expect(req.request.body).toEqual({ + benRegID: 'BEN1', + benVisitID: 'VIS1', + visitCode: 'VC001', + }); + req.flush({ data: {} }); + }); + }); + + describe('PNC methods', () => { + it('getPNCDetails should POST', () => { + service.getPNCDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getPNCDetailsUrl); + expect(req.request.body).toEqual({ + benRegID: 'BEN1', + benVisitID: 'VIS1', + visitCode: 'VC001', + }); + req.flush({ data: {} }); + }); + }); + + describe('postOncologistRemarksforCancerCaseSheet', () => { + it('should POST oncologist remarks', () => { + service + .postOncologistRemarksforCancerCaseSheet('remark', 'VIS1', 'REG1') + .subscribe(); + const req = httpMock.expectOne( + environment.updateOncologistRemarksCancelUrl + ); + expect(req.request.body.provisionalDiagnosisOncologist).toBe('remark'); + expect(req.request.body.beneficiaryRegID).toBe('REG1'); + expect(req.request.body.benVisitID).toBe('VIS1'); + expect(req.request.body.modifiedBy).toBe('testUser'); + req.flush({ statusCode: 200 }); + }); + }); + + describe('getVisitComplaintDetails (caching & visit-category branching)', () => { + it('should use Cancer Screening URL for Cancer Screening visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'Cancer Screening'; + return mockSessionStore[key] || null; + }); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningVisitDetails + ); + req.flush({ data: {} }); + }); + + it('should use General OPD QC URL for General OPD (QC) visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD (QC)'; + return mockSessionStore[key] || null; + }); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getGeneralOPDQuickConsultVisitDetails + ); + req.flush({ data: {} }); + }); + + it('should use ANC URL for ANC visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'ANC'; + return mockSessionStore[key] || null; + }); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getANCVisitDetailsUrl); + req.flush({ data: {} }); + }); + + it('should use cached value if getVisitComplaint is already set', () => { + service.getVisitComplaint = 'cachedValue'; + const result = service.getVisitComplaintDetails('BEN1', 'VIS1'); + expect(result).toBe('cachedValue'); + }); + + it('should use PNC URL for PNC visit (returns directly)', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'PNC'; + return mockSessionStore[key] || null; + }); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getPNCVisitDetailsUrl); + req.flush({ data: {} }); + }); + + it('should use COVID-19 URL for COVID-19 Screening visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'COVID-19 Screening'; + return mockSessionStore[key] || null; + }); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getCovidCareVisitDetailsUrl); + req.flush({ data: {} }); + }); + }); + + describe('getPregVisitComplaintDetails', () => { + it('should POST to NCD Screening visit details URL', () => { + service + .getPregVisitComplaintDetails('BEN1', 'VIS1', 'NCD screening') + .subscribe(); + const req = httpMock.expectOne(environment.getNCDScreeningVisitDetails); + expect(req.request.body.benRegID).toBe('BEN1'); + req.flush({ data: {} }); + }); + }); + + describe('getGeneralHistoryDetails (caching & visit-category branching)', () => { + it('should use ANC URL for ANC visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'ANC'; + return mockSessionStore[key] || null; + }); + service.generalHistory = null; + service.getGeneralHistoryDetails('REG1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getANCHistoryDetailsUrl); + req.flush({ data: {} }); + }); + + it('should use General OPD URL for General OPD visit', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD'; + return mockSessionStore[key] || null; + }); + service.generalHistory = null; + service.getGeneralHistoryDetails('REG1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getGeneralOPDHistoryDetailsUrl + ); + req.flush({ data: {} }); + }); + + it('should return cached value if generalHistory is set', () => { + service.generalHistory = 'cached'; + const result = service.getGeneralHistoryDetails('REG1', 'VIS1'); + expect(result).toBe('cached'); + }); + }); + + describe('getGenericVitals (visit-category branching)', () => { + const beneficiary = { benRegID: 'REG1', benVisitID: 'VIS1' }; + + it('should use General OPD QC URL for General OPD (QC)', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD (QC)'; + return mockSessionStore[key] || null; + }); + service.getGenericVitals(beneficiary).subscribe(); + const req = httpMock.expectOne( + environment.getGeneralOPDQuickConsultVitalDetails + ); + req.flush({ data: {} }); + }); + + it('should use ANC URL for ANC', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'ANC'; + return mockSessionStore[key] || null; + }); + service.getGenericVitals(beneficiary).subscribe(); + const req = httpMock.expectOne(environment.getANCVitalsDetailsUrl); + req.flush({ data: {} }); + }); + + it('should use NCD screening URL for NCD screening', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'NCD screening'; + return mockSessionStore[key] || null; + }); + service.getGenericVitals(beneficiary).subscribe(); + const req = httpMock.expectOne(environment.getNCDSceeriningVitalDetails); + req.flush({ data: {} }); + }); + + it('should complete without emitting for unknown visit category', done => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'Unknown'; + return mockSessionStore[key] || null; + }); + service.getGenericVitals(beneficiary).subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('getGeneralExamintionData (visit-category branching)', () => { + it('should use ANC URL for ANC', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'ANC'; + return mockSessionStore[key] || null; + }); + service.getGeneralExamintionData('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getANCExaminationDataUrl); + req.flush({ data: {} }); + }); + + it('should use General OPD URL for General OPD', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD'; + return mockSessionStore[key] || null; + }); + service.getGeneralExamintionData('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne( + environment.getGeneralOPDExaminationDetailsUrl + ); + req.flush({ data: {} }); + }); + + it('should use PNC URL for PNC', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'PNC'; + return mockSessionStore[key] || null; + }); + service.getGeneralExamintionData('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getPNCExaminationDataUrl); + req.flush({ data: {} }); + }); + + it('should complete without emit for unknown category', done => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'Unknown'; + return mockSessionStore[key] || null; + }); + service.getGeneralExamintionData('BEN1', 'VIS1').subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('updateGeneralVitals (visit-category branching)', () => { + const mockVitalsForm = { + value: { weight: 70, height: 170 }, + getRawValue: () => ({ weight: 70, height: 170 }), + }; + + it('should POST to ANC vitals URL for ANC', () => { + service.updateGeneralVitals(mockVitalsForm, 'ANC').subscribe(); + const req = httpMock.expectOne(environment.updateANCVitalsDetailsUrl); + expect(req.request.method).toBe('POST'); + req.flush({ statusCode: 200 }); + }); + + it('should POST to General OPD vitals URL for General OPD', () => { + service.updateGeneralVitals(mockVitalsForm, 'General OPD').subscribe(); + const req = httpMock.expectOne( + environment.updateGeneralOPDVitalsDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should POST to NCD care vitals URL for NCD care', () => { + service.updateGeneralVitals(mockVitalsForm, 'NCD care').subscribe(); + const req = httpMock.expectOne(environment.updateNCDCareVitalsDetailsUrl); + req.flush({ statusCode: 200 }); + }); + + it('should POST to PNC vitals URL for PNC', () => { + service.updateGeneralVitals(mockVitalsForm, 'PNC').subscribe(); + const req = httpMock.expectOne(environment.updatePNCVitalsDetailsUrl); + req.flush({ statusCode: 200 }); + }); + + it('should POST to COVID vitals URL for COVID-19 Screening', () => { + service + .updateGeneralVitals(mockVitalsForm, 'COVID-19 Screening') + .subscribe(); + const req = httpMock.expectOne( + environment.updateCovidCareVitalsDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should POST to NCD screening vitals URL for NCD screening', () => { + service.updateGeneralVitals(mockVitalsForm, 'NCD screening').subscribe(); + const req = httpMock.expectOne(environment.updateNCDVitalsDetailsUrl); + req.flush({ statusCode: 200 }); + }); + + it('should complete without emit for unknown category', done => { + service.updateGeneralVitals(mockVitalsForm, 'Unknown').subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('updateIDRSDetails', () => { + const mockIdrsForm = { + value: { idrsScore: 60 }, + }; + + it('should POST for NCD screening', () => { + service.updateIDRSDetails(mockIdrsForm, 'NCD screening').subscribe(); + const req = httpMock.expectOne( + environment.updateNCDScreeningIDRSDetailsUrl + ); + expect(req.request.body.idrsDetails).toBeDefined(); + req.flush({ statusCode: 200 }); + }); + + it('should complete without emit for non-NCD screening', done => { + service.updateIDRSDetails(mockIdrsForm, 'General OPD').subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('getIDRSDetails', () => { + it('should POST for NCD screening', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'NCD screening'; + return mockSessionStore[key] || null; + }); + service.getIDRSDetails('BEN1', 'VIS1').subscribe(); + const req = httpMock.expectOne(environment.getNCDScreeningIDRSDetails); + req.flush({ data: {} }); + }); + + it('should complete for non-NCD screening', done => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD'; + return mockSessionStore[key] || null; + }); + service.getIDRSDetails('BEN1', 'VIS1').subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('getCaseRecordAndReferDetails (caching & branching)', () => { + it('should use Cancer Screening URL', () => { + service.caseRecordAndReferDetails = null; + service + .getCaseRecordAndReferDetails('REG1', 'VIS1', 'Cancer Screening') + .subscribe(); + const req = httpMock.expectOne( + environment.getCancerScreeningDoctorDetails + ); + req.flush({ data: {} }); + }); + + it('should use ANC URL', () => { + service.caseRecordAndReferDetails = null; + service.getCaseRecordAndReferDetails('REG1', 'VIS1', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.getANCDoctorDetails); + req.flush({ data: {} }); + }); + + it('should use General OPD (QC) URL', () => { + service.caseRecordAndReferDetails = null; + service + .getCaseRecordAndReferDetails('REG1', 'VIS1', 'General OPD (QC)') + .subscribe(); + const req = httpMock.expectOne( + environment.getGeneralOPDQuickConsultDoctorDetails + ); + req.flush({ data: {} }); + }); + + it('should use cached value', () => { + service.caseRecordAndReferDetails = 'cached'; + const result = service.getCaseRecordAndReferDetails( + 'REG1', + 'VIS1', + 'ANC' + ); + expect(result).toBe('cached'); + }); + }); + + describe('Examination update helper methods', () => { + it('updateGeneralExaminationForm should merge form and details', () => { + const result = service.updateGeneralExaminationForm( + { consciousness: 'conscious' }, + { modifiedBy: 'test' } + ); + expect(result.consciousness).toBe('conscious'); + expect(result.modifiedBy).toBe('test'); + }); + + it('updateHeadToToeExaminationForm should merge', () => { + const result = service.updateHeadToToeExaminationForm( + { headExam: 'normal' }, + { modifiedBy: 'test' } + ); + expect(result.headExam).toBe('normal'); + expect(result.modifiedBy).toBe('test'); + }); + + it('updateCardioVascularSystemForm should merge', () => { + const result = service.updateCardioVascularSystemForm( + { heartRate: 72 }, + { modifiedBy: 'test' } + ); + expect(result.heartRate).toBe(72); + }); + + it('updateRespiratorySystemForm should merge', () => { + const result = service.updateRespiratorySystemForm( + { respiratoryRate: 18 }, + { modifiedBy: 'test' } + ); + expect(result.respiratoryRate).toBe(18); + }); + + it('updateCentralNervousSystemForm should merge', () => { + const result = service.updateCentralNervousSystemForm( + { cns: 'normal' }, + { modifiedBy: 'test' } + ); + expect(result.cns).toBe('normal'); + }); + + it('updateMusculoSkeletalSystemForm should merge', () => { + const result = service.updateMusculoSkeletalSystemForm( + { joints: 'normal' }, + { modifiedBy: 'test' } + ); + expect(result.joints).toBe('normal'); + }); + + it('updateGenitoUrinarySystemForm should merge', () => { + const result = service.updateGenitoUrinarySystemForm( + { kidney: 'normal' }, + { modifiedBy: 'test' } + ); + expect(result.kidney).toBe('normal'); + }); + + it('updateGastroIntestinalSystemForm should merge', () => { + const result = service.updateGastroIntestinalSystemForm( + { abdomen: 'soft' }, + { modifiedBy: 'test' } + ); + expect(result.abdomen).toBe('soft'); + }); + + it('updateANCObstetricExamination should merge', () => { + const result = service.updateANCObstetricExamination( + { fundalHeight: '30' }, + { modifiedBy: 'test' } + ); + expect(result.fundalHeight).toBe('30'); + }); + }); + + describe('History update helper methods', () => { + it('updateGeneralDevelopmentHistory should merge form value and details', () => { + const form = { value: { grossMotorMilestone: 'normal' } }; + const result = service.updateGeneralDevelopmentHistory(form, { + modifiedBy: 'test', + }); + expect(result.grossMotorMilestone).toBe('normal'); + expect(result.modifiedBy).toBe('test'); + }); + + it('updateGeneralMedicationHistory should merge', () => { + const form = { + value: { medicationHistoryList: [{ medication: 'aspirin' }] }, + }; + const result = service.updateGeneralMedicationHistory(form, { + modifiedBy: 'test', + }); + expect(result.medicationHistoryList).toBeDefined(); + }); + + it('updateGeneralImmunizationHistory should extract immunizationList', () => { + const form = { + value: { + immunizationList: [{ vaccine: 'BCG' }], + }, + }; + const result = service.updateGeneralImmunizationHistory(form, { + modifiedBy: 'test', + }); + expect(result.immunizationList).toEqual([{ vaccine: 'BCG' }]); + }); + + it('updateGeneralOtherVaccines should map vaccine names', () => { + const form = { + value: { + otherVaccines: [ + { + vaccineName: { vaccineID: 1, vaccineName: 'Hepatitis B' }, + }, + ], + }, + }; + const result = service.updateGeneralOtherVaccines(form, { + modifiedBy: 'test', + }); + expect(result.childOptionalVaccineList[0].vaccineID).toBe(1); + expect(result.childOptionalVaccineList[0].vaccineName).toBe( + 'Hepatitis B' + ); + }); + + it('updateGeneralFeedingHistory should handle foodIntoleranceStatus', () => { + const form = { + value: { foodIntoleranceStatus: '1', typeOfFood: 'milk' }, + }; + const result = service.updateGeneralFeedingHistory(form, { + modifiedBy: 'test', + }); + expect(result.foodIntoleranceStatus).toBe(1); + }); + + it('updateGeneralFeedingHistory should handle falsy foodIntoleranceStatus', () => { + const form = { + value: { foodIntoleranceStatus: '', typeOfFood: 'milk' }, + }; + const result = service.updateGeneralFeedingHistory(form, { + modifiedBy: 'test', + }); + expect(result.foodIntoleranceStatus).toBe(''); + }); + + it('updateGeneralPerinatalHistory should map placeOfDelivery', () => { + const form = { + value: { + placeOfDelivery: { + deliveryPlaceID: 1, + deliveryPlace: 'Hospital', + }, + typeOfDelivery: null, + complicationAtBirth: null, + }, + }; + const result = service.updateGeneralPerinatalHistory(form, { + modifiedBy: 'test', + }); + expect(result.deliveryPlaceID).toBe(1); + expect(result.placeOfDelivery).toBe('Hospital'); + }); + + it('updateGeneralPerinatalHistory should map typeOfDelivery', () => { + const form = { + value: { + placeOfDelivery: null, + typeOfDelivery: { + deliveryTypeID: 2, + deliveryType: 'Normal', + }, + complicationAtBirth: null, + }, + }; + const result = service.updateGeneralPerinatalHistory(form, {}); + expect(result.deliveryTypeID).toBe(2); + expect(result.typeOfDelivery).toBe('Normal'); + }); + + it('updateGeneralPerinatalHistory should map complicationAtBirth', () => { + const form = { + value: { + placeOfDelivery: null, + typeOfDelivery: null, + complicationAtBirth: { + birthComplicationID: 3, + name: 'None', + }, + }, + }; + const result = service.updateGeneralPerinatalHistory(form, {}); + expect(result.complicationAtBirthID).toBe(3); + expect(result.complicationAtBirth).toBe('None'); + }); + }); + + describe('Case record helper methods', () => { + it('postGeneralCaseRecordFindings should filter and map complaints', () => { + const findingForm = { + value: { + complaints: [ + { + chiefComplaint: { + chiefComplaintID: 1, + chiefComplaint: 'Fever', + }, + duration: 5, + }, + { chiefComplaint: null, duration: null }, + ], + }, + }; + const result = service.postGeneralCaseRecordFindings(findingForm, { + benRegID: '1', + }); + expect(result.complaints.length).toBe(1); + expect(result.complaints[0].chiefComplaintID).toBe(1); + expect(result.complaints[0].chiefComplaint).toBe('Fever'); + expect(result.complaints[0].duration).toBe('5'); + }); + + it('postANCCaseRecordDiagnosis should merge and remove null dateOfDeath', () => { + const diagForm = { + value: { diagnosis: 'normal', dateOfDeath: null }, + }; + const result = service.postANCCaseRecordDiagnosis(diagForm, { + benRegID: '1', + }); + expect(result.diagnosis).toBe('normal'); + expect(result.dateOfDeath).toBeUndefined(); + }); + + it('postANCCaseRecordDiagnosis should keep dateOfDeath if not null', () => { + const diagForm = { + value: { diagnosis: 'anemia', dateOfDeath: '2024-01-01' }, + }; + const result = service.postANCCaseRecordDiagnosis(diagForm, {}); + expect(result.dateOfDeath).toBe('2024-01-01'); + }); + + it('postGeneralOPDCaseRecordDiagnosis should merge', () => { + const diagForm = { value: { diagnosis: 'flu' } }; + const result = service.postGeneralOPDCaseRecordDiagnosis(diagForm, { + benRegID: '1', + }); + expect(result.diagnosis).toBe('flu'); + expect(result.benRegID).toBe('1'); + }); + + it('postNCDCareCaseRecordDiagnosis should map ncdCareType', () => { + const diagForm = { + value: { + ncdCareType: { + ncdCareTypeID: 1, + ncdCareType: 'Diabetes', + }, + }, + }; + const result = service.postNCDCareCaseRecordDiagnosis(diagForm, {}); + expect(result.ncdCareTypeID).toBe(1); + expect(result.ncdCareType).toBe('Diabetes'); + }); + + it('postGeneralCaseRecordInvestigation should concat lab and radiology tests', () => { + const investigationForm = { + value: { + labTest: [ + { procedureName: 'CBC', disabled: false }, + { procedureName: 'DisabledTest', disabled: true }, + ], + radiologyTest: [{ procedureName: 'X-Ray', disabled: false }], + }, + }; + const result = service.postGeneralCaseRecordInvestigation( + investigationForm, + {} + ); + expect(result.laboratoryList.length).toBe(2); + expect(result.labTest).toBeUndefined(); + expect(result.radiologyTest).toBeUndefined(); + }); + + it('postGeneralCaseRecordInvestigation should handle null radiologyTest', () => { + const investigationForm = { + value: { + labTest: [{ procedureName: 'CBC', disabled: false }], + radiologyTest: [], + }, + }; + const result = service.postGeneralCaseRecordInvestigation( + investigationForm, + {} + ); + expect(result.laboratoryList.length).toBe(1); + }); + + it('postGeneralCaseRecordPrescription should filter by createdBy', () => { + const prescriptionForm = { + value: { + prescribedDrugs: [ + { drugName: 'Paracetamol', createdBy: 'doc1' }, + { drugName: 'Empty', createdBy: null }, + ], + }, + }; + const result = service.postGeneralCaseRecordPrescription( + prescriptionForm, + {} + ); + expect(result.length).toBe(1); + expect(result[0].drugName).toBe('Paracetamol'); + }); + + it('postGeneralRefer should map referredToInstituteName', () => { + const referForm = { + value: { + referredToInstituteName: { + institutionID: 1, + institutionName: 'Hospital A', + }, + refrredToAdditionalServiceList: null, + revisitDate: null, + referralReason: null, + }, + controls: { + revisitDate: { value: null }, + referralReason: { value: null }, + }, + }; + const result = service.postGeneralRefer(referForm, {}); + expect(result.referredToInstituteID).toBe(1); + expect(result.referredToInstituteName).toBe('Hospital A'); + }); + + it('postGeneralRefer should filter disabled additional services', () => { + const referForm = { + value: { + referredToInstituteName: null, + refrredToAdditionalServiceList: [ + { serviceName: 'Lab', disabled: false }, + { serviceName: 'Disabled', disabled: true }, + ], + revisitDate: null, + referralReason: null, + }, + controls: { + revisitDate: { value: null }, + referralReason: { value: null }, + }, + }; + const result = service.postGeneralRefer(referForm, {}); + expect(result.refrredToAdditionalServiceList.length).toBe(1); + }); + }); + + describe('postGeneralCaseRecordDiagnosis (visit-category routing)', () => { + const diagForm = { value: { diagnosis: 'test' } }; + const otherDetails = { benRegID: '1' }; + + it('should route to ANC diagnosis for ANC', () => { + const result = service.postGeneralCaseRecordDiagnosis( + diagForm, + 'ANC', + otherDetails + ); + expect(result).toBeDefined(); + }); + + it('should route to General OPD diagnosis for General OPD', () => { + const result = service.postGeneralCaseRecordDiagnosis( + diagForm, + 'General OPD', + otherDetails + ); + expect(result).toBeDefined(); + }); + + it('should route to NCD care diagnosis for NCD care', () => { + const ncdForm = { + value: { + ncdCareType: { ncdCareTypeID: 1, ncdCareType: 'Diabetes' }, + }, + }; + const result = service.postGeneralCaseRecordDiagnosis( + ncdForm, + 'NCD care', + otherDetails + ); + expect(result.ncdCareTypeID).toBe(1); + }); + + it('should route to PNC (ANC handler) for PNC', () => { + const pncForm = { value: { diagnosis: 'pnc', dateOfDeath: null } }; + const result = service.postGeneralCaseRecordDiagnosis( + pncForm, + 'PNC', + otherDetails + ); + expect(result).toBeDefined(); + }); + + it('should return undefined for unknown category', () => { + const result = service.postGeneralCaseRecordDiagnosis( + diagForm, + 'Unknown', + otherDetails + ); + expect(result).toBeUndefined(); + }); + }); + + describe('updateGeneralHistory (visit-category branching)', () => { + const mockHistoryForm = { + controls: { + pastHistory: { value: { pastIllness: [], pastSurgery: [] } }, + comorbidityHistory: { + value: { comorbidityConcurrentConditionsList: [] }, + }, + medicationHistory: { value: {} }, + pastObstericHistory: { + value: { pastObstericHistoryList: [] }, + }, + menstrualHistory: { + value: {}, + getRawValue: () => ({}), + }, + familyHistory: { value: { familyDiseaseList: [] } }, + personalHistory: { + value: { + tobaccoList: [], + alcoholList: [], + allergicList: [], + }, + }, + otherVaccines: { value: { otherVaccines: [] } }, + immunizationHistory: { value: { immunizationList: [] } }, + developmentHistory: { value: {} }, + feedingHistory: { value: { foodIntoleranceStatus: '' } }, + perinatalHistory: { + value: { + placeOfDelivery: null, + typeOfDelivery: null, + complicationAtBirth: null, + }, + }, + }, + }; + const temp = { modifiedBy: 'test' }; + + it('should POST to ANC history URL and remove child-specific fields', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'ANC'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe(); + const req = httpMock.expectOne(environment.updateANCHistoryDetailsUrl); + expect(req.request.body.feedingHistory).toBeUndefined(); + expect(req.request.body.developmentHistory).toBeUndefined(); + expect(req.request.body.perinatalHistroy).toBeUndefined(); + req.flush({ statusCode: 200 }); + }); + + it('should POST to General OPD history URL', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe(); + const req = httpMock.expectOne( + environment.updateGeneralOPDHistoryDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should POST to NCD care history URL', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'NCD care'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe(); + const req = httpMock.expectOne( + environment.updateNCDCareHistoryDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should POST to PNC history URL', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'PNC'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe(); + const req = httpMock.expectOne(environment.updatePNCHistoryDetailsUrl); + req.flush({ statusCode: 200 }); + }); + + it('should POST to COVID history URL', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'COVID-19 Screening'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe(); + const req = httpMock.expectOne( + environment.updateCovidCareHistoryDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should complete for unknown visit category', done => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'Unknown'; + return mockSessionStore[key] || null; + }); + service.updateGeneralHistory(mockHistoryForm, temp, 30).subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('Cancer history helpers', () => { + it('cancerFamilyHistoryForm should return undefined when form is not dirty', () => { + const form = { dirty: false, value: { diseases: null } }; + const result = service.cancerFamilyHistoryForm(form, {}); + expect(result).toBeUndefined(); + }); + + it('cancerFamilyHistoryForm should map diseases when dirty and non-null', () => { + const form = { + dirty: true, + value: { + diseases: [ + { + cancerDiseaseType: { + cancerDiseaseType: 'Lung Cancer', + }, + otherDiseaseType: null, + }, + ], + }, + }; + const result = service.cancerFamilyHistoryForm(form, { + benRegID: '1', + }); + expect(result[0].cancerDiseaseType).toBe('Lung Cancer'); + }); + + it('cancerFamilyHistoryForm should use otherDiseaseType for Any other Cancer', () => { + const form = { + dirty: true, + value: { + diseases: [ + { + cancerDiseaseType: { + cancerDiseaseType: 'Any other Cancer', + }, + otherDiseaseType: 'Rare Cancer', + }, + ], + }, + }; + const result = service.cancerFamilyHistoryForm(form, {}); + expect(result[0].cancerDiseaseType).toBe('Rare Cancer'); + }); + + it('cancerObstetricHistoryForm should return undefined when not dirty', () => { + const form = { dirty: false, value: {} }; + const result = service.cancerObstetricHistoryForm(form, {}); + expect(result).toBeUndefined(); + }); + + it('cancerObstetricHistoryForm should merge when dirty', () => { + const form = { dirty: true, value: { pregnancyStatus: 'no' } }; + const result = service.cancerObstetricHistoryForm(form, { + benRegID: '1', + }); + expect(result.pregnancyStatus).toBe('no'); + expect(result.benRegID).toBe('1'); + }); + + it('cancerPersonalHistoryForm should return undefined when not dirty', () => { + const form = { dirty: false, value: {} }; + const result = service.cancerPersonalHistoryForm(form, {}); + expect(result).toBeUndefined(); + }); + + it('cancerPersonalHistoryForm should merge when dirty', () => { + const form = { dirty: true, value: { tobaccoUse: 'yes' } }; + const result = service.cancerPersonalHistoryForm(form, { + benRegID: '1', + }); + expect(result.tobaccoUse).toBe('yes'); + }); + }); + + describe('updateANCDetails', () => { + it('should POST updated ANC details', () => { + const patientANCForm = { + controls: { + patientANCDetailsForm: { + value: { lmpDate: null }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 1, + termDeliveries_T: 0, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 0, + bloodGroup: 'O+', + }, + }, + patientANCImmunizationForm: { + value: { tT_1Status: 'done' }, + }, + }, + }; + service.updateANCDetails(patientANCForm, { benRegID: '1' }).subscribe(); + const req = httpMock.expectOne(environment.updateANCDetailsUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body.ancObstetricDetails).toBeDefined(); + expect(req.request.body.ancImmunization).toBeDefined(); + req.flush({ statusCode: 200 }); + }); + }); + + describe('updatePNCDetails', () => { + it('should POST updated PNC details with mapped fields', () => { + const patientPNCForm = { + value: { + deliveryPlace: { + deliveryPlaceID: 1, + deliveryPlace: 'Hospital', + }, + deliveryType: { + deliveryTypeID: 2, + deliveryType: 'Normal', + }, + deliveryComplication: null, + pregOutcome: null, + postNatalComplication: null, + gestationName: null, + newBornHealthStatus: null, + }, + }; + service.updatePNCDetails(patientPNCForm, { benRegID: '1' }).subscribe(); + const req = httpMock.expectOne(environment.updatePNCDetailsUrl); + expect(req.request.body.PNCDetails.deliveryPlaceID).toBe(1); + expect(req.request.body.PNCDetails.deliveryPlace).toBe('Hospital'); + req.flush({ statusCode: 200 }); + }); + }); + + describe('updateNCDScreeningHistory', () => { + const mockHistoryForm = { + controls: { + familyHistory: { value: { familyDiseaseList: [] } }, + physicalActivityHistory: { value: { activityType: 'moderate' } }, + personalHistory: { + value: { + tobaccoList: [], + alcoholList: [], + allergicList: [], + }, + }, + }, + }; + + it('should POST for NCD screening', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'NCD screening'; + return mockSessionStore[key] || null; + }); + service.updateNCDScreeningHistory(mockHistoryForm, {}, 30).subscribe(); + const req = httpMock.expectOne( + environment.updateNCDScreeningHistoryDetailsUrl + ); + req.flush({ statusCode: 200 }); + }); + + it('should complete for non-NCD screening', done => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'visitCategory') return 'General OPD'; + return mockSessionStore[key] || null; + }); + service.updateNCDScreeningHistory(mockHistoryForm, {}, 30).subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('updatePhyscialActivityHistory', () => { + it('should merge form value and otherDetails', () => { + const form = { value: { activityType: 'moderate' } }; + const result = service.updatePhyscialActivityHistory(form, { + modifiedBy: 'test', + }); + expect(result.activityType).toBe('moderate'); + expect(result.modifiedBy).toBe('test'); + }); + }); + + describe('saveSpecialistCancerObservation', () => { + it('should POST specialist cancer observation', () => { + const specialistForm = { + controls: { + patientCaseRecordForm: { value: { diagnosis: 'test' } }, + patientReferForm: { value: { remarks: 'note' } }, + }, + }; + service + .saveSpecialistCancerObservation( + specialistForm, + { benRegID: '1' }, + true + ) + .subscribe(); + const req = httpMock.expectOne( + environment.saveSpecialistCancerObservationUrl + ); + expect(req.request.body.diagnosis).toBeDefined(); + expect(req.request.body.diagnosis.doctorSignatureFlag).toBeTrue(); + req.flush({ statusCode: 200 }); + }); + }); + + describe('postGenericVisitDetailForm', () => { + it('should handle NCD screening', () => { + const patientVisitForm = { + controls: { + patientVisitDetailsForm: { value: { visitNo: 1 } }, + patientFileUploadDetailsForm: { value: {} }, + }, + }; + const result: any = service.postGenericVisitDetailForm( + patientVisitForm, + null, + 'NCD screening' + ); + expect(result.visitDetails).toBeDefined(); + }); + + it('should complete for unknown category', done => { + const patientVisitForm = { controls: {} }; + const result = service.postGenericVisitDetailForm( + patientVisitForm, + null, + 'Unknown' + ); + result.subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('postPatientVisitDetails', () => { + it('should merge visit and files with session data', () => { + const result = service.postPatientVisitDetails( + { visitNo: 1 }, + { fileIDs: [] } + ); + expect(result.visitNo).toBe(1); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.providerServiceMapID).toBe('1234'); + expect(result.createdBy).toBe('testUser'); + }); + }); + + describe('additional coverage', () => { + const withCategory = (cat: string) => + sessionStorageSpy.getItem.and.callFake((key: string) => + key === 'visitCategory' ? cat : mockSessionStore[key] || null + ); + + beforeEach(() => spyOn(console, 'log')); + + const medicalForm = (diagnosis: any = { provisionalDiagnosis: 'x' }) => + new FormGroup({ + patientCaseRecordForm: new FormGroup({ + generalFindingsForm: new FormGroup({ + complaints: new FormControl([ + { + chiefComplaint: { + chiefComplaintID: 4, + chiefComplaint: 'Fever', + }, + duration: 3, + }, + { chiefComplaint: null, duration: 1 }, + ]), + }), + generalDoctorInvestigationForm: new FormGroup({ + labTest: new FormControl([ + { procedureID: 1 }, + { procedureID: 2, disabled: true }, + ]), + radiologyTest: new FormControl([{ procedureID: 3 }]), + }), + drugPrescriptionForm: new FormGroup({ + prescribedDrugs: new FormControl([ + { drugID: 1, createdBy: 'doc' }, + { drugID: 2 }, + ]), + }), + generalDiagnosisForm: new FormGroup({ + ...Object.keys(diagnosis).reduce( + (acc: any, k) => ({ ...acc, [k]: new FormControl(diagnosis[k]) }), + {} + ), + }), + }), + patientReferForm: new FormGroup({ + referredToInstituteName: new FormControl({ + institutionID: 8, + institutionName: 'PHC', + }), + refrredToAdditionalServiceList: new FormControl([ + { serviceID: 1 }, + { serviceID: 2, disabled: true }, + ]), + revisitDate: new FormControl('2024-01-01'), + referralReason: new FormControl('reason'), + }), + patientVisitForm: new FormGroup({ + patientVisitDetailsForm: new FormControl({ visitReason: 'R' }), + patientFileUploadDetailsForm: new FormControl({ fileIDs: [1] }), + }), + }); + + const checkCommon = (body: any) => { + expect(body.findings.complaints).toEqual([ + { chiefComplaint: 'Fever', chiefComplaintID: 4, duration: '3' }, + ]); + expect(body.investigation.laboratoryList).toEqual([ + { procedureID: 1 }, + { procedureID: 3 }, + ]); + expect(body.prescription).toEqual([{ drugID: 1, createdBy: 'doc' }]); + expect(body.refer.referredToInstituteID).toBe(8); + expect(body.refer.referredToInstituteName).toBe('PHC'); + expect(body.refer.refrredToAdditionalServiceList).toEqual([ + { serviceID: 1 }, + ]); + expect(body.refer.revisitDate).toBe('2024-01-01'); + expect(body.refer.referralReason).toBe('reason'); + expect(body.vanID).toBe(10); + expect(body.parkingPlaceID).toBe(20); + expect(body.beneficiaryRegID).toBe('9876'); + }; + + it('postDoctorCancerVisitDetails merges case record, refer and session details', () => { + const form = { + controls: { + patientCaseRecordForm: { value: { remarks: 'r' } }, + patientReferForm: { value: { referralReason: 'x' } }, + }, + }; + service.postDoctorCancerVisitDetails(form, { tc: 1 }, true).subscribe(); + const req = httpMock.expectOne( + environment.saveDoctorCancerScreeningDetails + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body.tcRequest).toEqual({ tc: 1 }); + expect(req.request.body.doctorSignatureFlag).toBeTrue(); + expect(req.request.body.diagnosis.remarks).toBe('r'); + expect(req.request.body.diagnosis.referralReason).toBe('x'); + expect(req.request.body.diagnosis.vanID).toBe(10); + expect(req.request.body.diagnosis.beneficiaryRegID).toBe('9876'); + req.flush({}); + }); + + describe('updateCancerExaminationDetails', () => { + const exam = ( + dirty: boolean, + lesions: any = ['A', 'Any other lesion'] + ) => { + const g = (value: any) => ({ dirty, value }); + return { + controls: { + signsForm: new FormGroup({ + shortnessOfBreath: new FormControl(true), + lymphNodes: new FormArray([ + new FormGroup({ lymphNodeName: new FormControl('Neck') }), + ]), + }), + oralExaminationForm: g({ + preMalignantLesionTypeList: lesions, + otherLesionType: 'Custom', + image: 'img', + }), + breastExaminationForm: g({ lump: true, image: 'i' }), + abdominalExaminationForm: g({ liver: 'ok', image: 'i' }), + gynecologicalExaminationForm: g({ cervix: 'ok', image: 'i' }), + }, + }; + }; + + it('builds every dirty section and image coordinates', () => { + const form: any = exam(true); + form.controls.signsForm.markAsDirty(); + service + .updateCancerExaminationDetails(form, { visitCode: 'V' }, [{ x: 1 }]) + .subscribe(); + const req = httpMock.expectOne( + environment.updateCancerScreeningExamination + ); + const b = req.request.body; + expect( + b.signsDetails.cancerSignAndSymptoms.shortnessOfBreath + ).toBeTrue(); + expect(b.signsDetails.cancerLymphNodeDetails[0]).toEqual({ + lymphNodeName: 'Neck', + visitCode: 'V', + createdBy: 'testUser', + }); + expect(b.oralDetails.preMalignantLesionTypeList).toEqual([ + 'A', + 'Custom', + ]); + expect(b.oralDetails.image).toBeUndefined(); + expect(b.breastDetails.lump).toBeTrue(); + expect(b.abdominalDetails.liver).toBe('ok'); + expect(b.gynecologicalDetails.cervix).toBe('ok'); + expect(b.imageCoordinates).toEqual([{ x: 1 }]); + expect(b.vanID).toBe(10); + req.flush({}); + }); + + it('keeps lesion list when "Any other lesion" is not last and skips clean sections', () => { + const form: any = exam(true, ['Any other lesion', 'B']); + service.updateCancerExaminationDetails(form, {}, []).subscribe(); + const req = httpMock.expectOne( + environment.updateCancerScreeningExamination + ); + expect(req.request.body.signsDetails).toBeUndefined(); + expect(req.request.body.oralDetails.preMalignantLesionTypeList).toEqual( + ['Any other lesion', 'B'] + ); + expect(req.request.body.imageCoordinates).toBeUndefined(); + req.flush({}); + }); + + it('posts nothing-but-null when no section is dirty', () => { + service + .updateCancerExaminationDetails(exam(false, null), {}, []) + .subscribe(); + const req = httpMock.expectOne( + environment.updateCancerScreeningExamination + ); + expect(req.request.body).toBeNull(); + req.flush({}); + }); + + it('handles a null lesion list on a dirty oral form', () => { + service + .updateCancerExaminationDetails(exam(true, null), {}, []) + .subscribe(); + const req = httpMock.expectOne( + environment.updateCancerScreeningExamination + ); + expect( + req.request.body.oralDetails.preMalignantLesionTypeList + ).toBeNull(); + req.flush({}); + }); + }); + + it('updateCancerVitalsDetails merges vitals, disabled values and session data', () => { + service + .updateCancerVitalsDetails({ weight: 50 }, { height: 160 }) + .subscribe(); + const req = httpMock.expectOne(environment.updateCancerScreeningVitals); + expect(req.request.body).toEqual( + jasmine.objectContaining({ + weight: 50, + height: 160, + beneficiaryRegID: '9876', + modifiedBy: 'testUser', + vanID: 10, + parkingPlaceID: 20, + visitCode: 'VC001', + }) + ); + req.flush({}); + }); + + it('updateCancerHistoryDetails maps family, personal and obstetric history', () => { + const form = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { + dirty: true, + value: { + diseases: [ + { cancerDiseaseType: { cancerDiseaseType: 'Breast' } }, + { + cancerDiseaseType: { cancerDiseaseType: 'Any other Cancer' }, + otherDiseaseType: 'Rare', + }, + ], + }, + }, + cancerPatientPerosnalHistoryForm: { + dirty: true, + value: { smoke: 1 }, + }, + cancerPatientObstetricHistoryForm: { dirty: false, value: {} }, + }, + }; + service.updateCancerHistoryDetails(form, { visitCode: 'V' }).subscribe(); + const req = httpMock.expectOne(environment.updateCancerScreeningHistory); + expect(req.request.body.familyHistory).toEqual([ + { cancerDiseaseType: 'Breast', visitCode: 'V' }, + { cancerDiseaseType: 'Rare', otherDiseaseType: 'Rare', visitCode: 'V' }, + ]); + expect(req.request.body.personalHistory).toEqual({ + smoke: 1, + visitCode: 'V', + }); + expect(req.request.body.pastObstetricHistory).toBeUndefined(); + req.flush({}); + }); + + it('postQuickConsultDetails posts quick consultation with session data', () => { + service + .postQuickConsultDetails( + { quickConsultation: { a: 1 } }, + { t: 1 }, + false + ) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorGeneralQuickConsult); + const qc = req.request.body.quickConsultation; + expect(qc.a).toBe(1); + expect(qc.tcRequest).toEqual({ t: 1 }); + expect(qc.doctorSignatureFlag).toBeFalse(); + expect(qc.vanID).toBe(10); + expect(qc.createdBy).toBe('testUser'); + req.flush({}); + }); + + it('updateQuickConsultDetails posts quick consultation with specialist flag', () => { + service + .updateQuickConsultDetails( + { quickConsultation: { a: 2 } }, + null, + true, + true + ) + .subscribe(); + const req = httpMock.expectOne( + environment.updateGeneralOPDQuickConsultDoctorDetails + ); + const qc = req.request.body.quickConsultation; + expect(qc.a).toBe(2); + expect(qc.isSpecialist).toBeTrue(); + expect(qc.doctorSignatureFlag).toBeTrue(); + expect(qc.parkingPlaceID).toBe(20); + req.flush({}); + }); + + it('updateNCDScreeningDetails flattens masters and computes prescribed tests', () => { + service + .updateNCDScreeningDetails( + { + reasonForScreening: { + ncdScreeningReasonID: 1, + ncdScreeningReason: 'Camp', + }, + diabeticStatus: { + bpAndDiabeticStatusID: 2, + bpAndDiabeticStatus: 'Known', + }, + bloodPressureStatus: { + bpAndDiabeticStatusID: 3, + bpAndDiabeticStatus: 'New', + }, + labTestOrders: [ + { procedureName: 'BP Measurement' }, + { procedureName: 'Blood Glucose Measurement' }, + ], + }, + { patientFileUploadDetailsForm: { fileIDs: [9] } } + ) + .subscribe(); + const req = httpMock.expectOne(environment.updateNCDScreeningDetails); + const b = req.request.body; + expect(b.ncdScreeningReasonID).toBe(1); + expect(b.reasonForScreening).toBe('Camp'); + expect(b.diabeticStatusID).toBe(2); + expect(b.diabeticStatus).toBe('Known'); + expect(b.bloodPressureStatusID).toBe(3); + expect(b.bloodPressureStatus).toBe('New'); + expect(b.isBPPrescribed).toBeTrue(); + expect(b.isBloodGlucosePrescribed).toBeTrue(); + expect(b.fileIDs).toEqual([9]); + expect(b.benFlowID).toBe('222'); + req.flush({}); + }); + + it('updateNCDScreeningDetails leaves fields alone when masters are absent', () => { + service + .updateNCDScreeningDetails( + { labTestOrders: [{ procedureName: 'X' }] }, + {} + ) + .subscribe(); + const req = httpMock.expectOne(environment.updateNCDScreeningDetails); + expect(req.request.body.isBPPrescribed).toBeFalse(); + expect(req.request.body.isBloodGlucosePrescribed).toBeFalse(); + expect(req.request.body.ncdScreeningReasonID).toBeUndefined(); + req.flush({}); + }); + + it('postDoctorANCDetails builds the full ANC payload and drops null dateOfDeath', () => { + service + .postDoctorANCDetails( + medicalForm({ provisionalDiagnosis: 'p', dateOfDeath: null }), + { createdBy: 'doc' }, + { tc: 1 }, + true + ) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorANCDetails); + checkCommon(req.request.body); + expect(req.request.body.diagnosis.provisionalDiagnosis).toBe('p'); + expect('dateOfDeath' in req.request.body.diagnosis).toBeFalse(); + expect(req.request.body.tcRequest).toEqual({ tc: 1 }); + req.flush({}); + }); + + it('postDoctorGeneralOPDDetails builds the General OPD payload', () => { + service + .postDoctorGeneralOPDDetails( + medicalForm(), + { createdBy: 'doc' }, + null, + false + ) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorGeneralOPDDetails); + checkCommon(req.request.body); + expect(req.request.body.doctorSignatureFlag).toBeFalse(); + req.flush({}); + }); + + it('postDoctorNCDCareDetails flattens ncdCareType', () => { + service + .postDoctorNCDCareDetails( + medicalForm({ ncdCareType: { ncdCareTypeID: 5, ncdCareType: 'DM' } }), + {}, + null, + true + ) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorNCDCareDetails); + checkCommon(req.request.body); + expect(req.request.body.diagnosis.ncdCareTypeID).toBe(5); + expect(req.request.body.diagnosis.ncdCareType).toBe('DM'); + req.flush({}); + }); + + it('postDoctorCovidCareDetails builds the covid payload', () => { + service + .postDoctorCovidCareDetails(medicalForm(), {}, { tc: 2 }) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorCovidCareDetails); + checkCommon(req.request.body); + expect(req.request.body.tcRequest).toEqual({ tc: 2 }); + req.flush({}); + }); + + it('postDoctorNCDScreeningDetails includes visit details for NCD screening', () => { + withCategory('NCD screening'); + service + .postDoctorNCDScreeningDetails(medicalForm(), {}, null, true) + .subscribe(); + const req = httpMock.expectOne(environment.saveDoctorNCDScreeningDetails); + checkCommon(req.request.body); + expect(req.request.body.visitDetails.visitDetails.visitReason).toBe('R'); + expect(req.request.body.visitDetails.visitDetails.fileIDs).toEqual([1]); + req.flush({}); + }); + + it('postDoctorPNCDetails builds the PNC payload', () => { + service + .postDoctorPNCDetails( + medicalForm({ dateOfDeath: '2024' }), + {}, + null, + true + ) + .subscribe(); + const req = httpMock.expectOne(environment.savePNCDoctorDetailsUrl); + checkCommon(req.request.body); + expect(req.request.body.diagnosis.dateOfDeath).toBe('2024'); + req.flush({}); + }); + + describe('updateDoctorDiagnosisDetails routes by visit category', () => { + const cases: [string, string][] = [ + ['ANC', 'updateANCDoctorDetails'], + ['General OPD', 'updateGeneralOPDDoctorDetails'], + ['NCD care', 'updateNCDCareDoctorDetails'], + ['PNC', 'updatePNCDoctorDetails'], + ['COVID-19 Screening', 'updateCovidDoctorDetails'], + ['NCD screening', 'updateNCDScreeningDoctorDetails'], + ]; + cases.forEach(([cat, key]) => { + it(`posts to ${key} for ${cat}`, () => { + service + .updateDoctorDiagnosisDetails( + medicalForm({ provisionalDiagnosis: cat }), + cat, + { isSpecialist: true }, + null, + false + ) + .subscribe(); + const req = httpMock.expectOne((environment as any)[key]); + checkCommon(req.request.body); + expect(req.request.body.diagnosis.provisionalDiagnosis).toBe(cat); + expect(req.request.body.isSpecialist).toBeTrue(); + req.flush({}); + }); + }); + + it('completes without a request for unknown category', done => { + service + .updateDoctorDiagnosisDetails(medicalForm(), 'Other', {}, null, false) + .subscribe({ + next: () => fail('should not emit'), + complete: () => { + httpMock.expectNone(() => true); + done(); + }, + }); + }); + }); + + describe('updatePatientExamination', () => { + const exam = { + generalExaminationForm: { a: 1 }, + headToToeExaminationForm: { b: 1 }, + systemicExaminationForm: { + gastroIntestinalSystemForm: { g: 1 }, + cardioVascularSystemForm: { c: 1 }, + respiratorySystemForm: { r: 1 }, + centralNervousSystemForm: { n: 1 }, + musculoSkeletalSystemForm: { m: 1 }, + genitoUrinarySystemForm: { u: 1 }, + obstetricExaminationForANCForm: { o: 1 }, + }, + }; + const cases: [string, string][] = [ + ['ANC', 'updateANCExaminationDetailsUrl'], + ['PNC', 'updatePNCExaminationDetailsUrl'], + ['General OPD', 'updateGeneralOPDExaminationDetailsUrl'], + ]; + cases.forEach(([cat, key]) => { + it(`posts ${cat} examination`, () => { + service + .updatePatientExamination(exam, cat, { modifiedBy: 'u' }) + .subscribe(); + const req = httpMock.expectOne((environment as any)[key]); + const b = req.request.body; + expect(b.generalExamination).toEqual({ a: 1, modifiedBy: 'u' }); + expect(b.cardioVascularExamination).toEqual({ + c: 1, + modifiedBy: 'u', + }); + expect(b.genitoUrinarySystemExamination).toEqual({ + u: 1, + modifiedBy: 'u', + }); + expect(b.vanID).toBe(10); + if (cat === 'ANC') { + expect(b.obstetricExamination).toEqual({ o: 1, modifiedBy: 'u' }); + } else { + expect(b.gastroIntestinalExamination).toEqual({ + g: 1, + modifiedBy: 'u', + }); + } + req.flush({}); + }); + }); + + it('completes for other categories', done => { + service.updatePatientExamination(exam, 'NCD care', {}).subscribe({ + next: () => fail('should not emit'), + complete: () => done(), + }); + }); + }); + + describe('cached getters for remaining categories', () => { + const complaint: [string, string][] = [ + ['General OPD', 'getGeneralOPDVisitDetailsUrl'], + ['NCD screening', 'getNCDScreeningVisitDetails'], + ['NCD care', 'getNCDCareVisitDetailsUrl'], + ['COVID-19 Screening', 'getCovidCareVisitDetailsUrl'], + ]; + complaint.forEach(([cat, key]) => { + it(`getVisitComplaintDetails uses ${key} for ${cat}`, () => { + withCategory(cat); + service.getVisitComplaint = null; + service.getVisitComplaintDetails('B', 'V').subscribe(); + const req = httpMock.expectOne((environment as any)[key]); + expect(req.request.body).toEqual({ + benRegID: 'B', + benVisitID: 'V', + visitCode: 'VC001', + }); + req.flush({}); + }); + }); + + const history: [string, string][] = [ + ['NCD care', 'getNCDCareHistoryDetailsUrl'], + ['PNC', 'getPNCHistoryDetailsUrl'], + ['COVID-19 Screening', 'getCovidCareHistoryDetailsUrl'], + ['NCD screening', 'getNCDScreeningHistoryDetails'], + ['ANC', 'getANCHistoryDetailsUrl'], + ['General OPD', 'getGeneralOPDHistoryDetailsUrl'], + ]; + history.forEach(([cat, key]) => { + it(`getGeneralHistoryDetails uses ${key} for ${cat}`, () => { + withCategory(cat); + service.generalHistory = null; + service.getGeneralHistoryDetails('B', 'V').subscribe(); + httpMock.expectOne((environment as any)[key]).flush({}); + }); + }); + + const vitals: [string, string][] = [ + ['General OPD', 'getGeneralOPDVitalDetailsUrl'], + ['NCD care', 'getNCDCareVitalDetailsUrl'], + ['PNC', 'getPNCVitalsDetailsUrl'], + ['COVID-19 Screening', 'getCovidCareVitalDetailsUrl'], + ['NCD screening', 'getNCDSceeriningVitalDetails'], + ]; + vitals.forEach(([cat, key]) => { + it(`getGenericVitals uses ${key} for ${cat}`, () => { + withCategory(cat); + service.getGenericVitals({ benRegID: 'B' }).subscribe(); + const req = httpMock.expectOne((environment as any)[key]); + expect(req.request.body).toEqual({ + benRegID: 'B', + visitCode: 'VC001', + }); + req.flush({}); + }); + }); + + const caseRecord: [string, string][] = [ + ['General OPD', 'getGeneralOPDDoctorDetails'], + ['NCD screening', 'getNCDScreeningDoctorDetails'], + ['NCD care', 'getNCDCareDoctorDetails'], + ['PNC', 'getPNCDoctorDetails'], + ['COVID-19 Screening', 'getCovidDoctorDetails'], + ]; + caseRecord.forEach(([cat, key]) => { + it(`getCaseRecordAndReferDetails uses ${key} for ${cat}`, () => { + service.caseRecordAndReferDetails = null; + service.getCaseRecordAndReferDetails('B', 'V', cat).subscribe(); + httpMock.expectOne((environment as any)[key]).flush({}); + }); + }); + }); + + describe('history mapping helpers with populated lists', () => { + it('updateGeneralPastHistory maps illness and surgery types', () => { + const res = service.updateGeneralPastHistory( + { + value: { + pastIllness: [ + { + illnessType: { illnessType: 'TB', illnessID: 3 }, + timePeriodAgo: 2, + }, + { + illnessType: { illnessType: 'X', illnessID: 4 }, + timePeriodAgo: null, + }, + { illnessType: null }, + ], + pastSurgery: [ + { + surgeryType: { surgeryType: 'Appendix', surgeryID: 7 }, + timePeriodAgo: 1, + }, + { surgeryType: { surgeryType: 'Y', surgeryID: 8 } }, + { surgeryType: null }, + ], + }, + }, + { o: 1 } + ); + expect(res.pastIllness[0]).toEqual({ + illnessType: 'TB', + illnessTypeID: '3', + timePeriodAgo: '2', + }); + expect(res.pastIllness[1].timePeriodAgo).toBeNull(); + expect(res.pastSurgery[0]).toEqual({ + surgeryType: 'Appendix', + surgeryID: '7', + timePeriodAgo: '1', + }); + expect(res.pastSurgery[1].timePeriodAgo).toBeNull(); + expect(res.o).toBe(1); + }); + + it('updateGeneralComorbidityHistory maps conditions', () => { + const res = service.updateGeneralComorbidityHistory( + { + value: { + comorbidityConcurrentConditionsList: [ + { + comorbidConditions: { + comorbidCondition: 'DM', + comorbidConditionID: 2, + }, + }, + { comorbidConditions: { comorbidCondition: 'Other' } }, + ], + }, + }, + { o: 1 } + ); + expect(res.o).toBe(1); + expect(res.comorbidityConcurrentConditionsList[0]).toEqual({ + comorbidConditions: undefined, + comorbidCondition: 'DM', + comorbidConditionID: '2', + }); + expect( + res.comorbidityConcurrentConditionsList[1].comorbidConditionID + ).toBeUndefined(); + }); + + it('updateGeneralPersonalHistory maps tobacco, alcohol and allergies', () => { + const res = service.updateGeneralPersonalHistory( + { + value: { + riskySexualPracticesStatus: '1', + tobaccoList: [ + { + tobaccoUseType: { + personalHabitTypeID: 1, + habitValue: 'Beedi', + }, + }, + { tobaccoUseType: null }, + ], + alcoholList: [ + { + typeOfAlcohol: { personalHabitTypeID: 2, habitValue: 'Beer' }, + avgAlcoholConsumption: { habitValue: '1-2' }, + }, + {}, + ], + allergicList: [ + { + allergyType: { allergyType: 'Food' }, + typeOfAllergicReactions: [{ allergicReactionTypeID: 5 }], + }, + {}, + ], + }, + }, + { o: 1 } + ); + expect(res.riskySexualPracticesStatus).toBe(1); + expect(res.tobaccoList[0]).toEqual({ + tobaccoUseTypeID: 1, + tobaccoUseType: 'Beedi', + }); + expect(res.alcoholList[0]).toEqual({ + alcoholTypeID: 2, + typeOfAlcohol: 'Beer', + avgAlcoholConsumption: '1-2', + }); + expect(res.allergicList[0].allergyType).toBe('Food'); + expect( + res.allergicList[0].typeOfAllergicReactions[0].allergicReactionTypeID + ).toBe('5'); + }); + + it('updateGeneralPersonalHistory handles missing lists and status', () => { + const res = service.updateGeneralPersonalHistory({ value: {} }, {}); + expect(res.riskySexualPracticesStatus).toBeNull(); + expect(res.tobaccoList).toBeUndefined(); + }); + + it('updateGeneralFamilyHistory maps disease types', () => { + const res = service.updateGeneralFamilyHistory( + { + value: { + familyDiseaseList: [ + { diseaseType: { diseaseTypeID: 3, diseaseType: 'DM' } }, + { diseaseType: null }, + ], + }, + }, + { o: 1 } + ); + expect(res.familyDiseaseList[0]).toEqual({ + diseaseTypeID: '3', + diseaseType: 'DM', + }); + expect(res.o).toBe(1); + }); + + it('updateGeneralMenstrualHistory maps masters and adjusts LMP date', () => { + const res = service.updateGeneralMenstrualHistory( + { + getRawValue: () => ({ + menstrualCycleStatus: { + menstrualCycleStatusID: 1, + name: 'Regular', + }, + cycleLength: { menstrualRangeID: 2, menstrualCycleRange: '28' }, + bloodFlowDuration: { + menstrualRangeID: 3, + menstrualCycleRange: '5', + }, + lMPDate: '2024-01-10T00:00:00.000Z', + }), + }, + { o: 1 } + ); + expect(res.menstrualCycleStatusID).toBe('1'); + expect(res.menstrualCycleStatus).toBe('Regular'); + expect(res.menstrualCyclelengthID).toBe('2'); + expect(res.cycleLength).toBe('28'); + expect(res.menstrualFlowDurationID).toBe('3'); + expect(res.bloodFlowDuration).toBe('5'); + expect(typeof res.lMPDate).toBe('string'); + expect(res.o).toBe(1); + }); + + it('updateGeneralMenstrualHistory drops invalid LMP date', () => { + const res = service.updateGeneralMenstrualHistory( + { getRawValue: () => ({ lMPDate: 'Invalid Date' }) }, + {} + ); + expect('lMPDate' in res).toBeFalse(); + }); + + it('updateGeneralPastObstetricHistory maps every master', () => { + const res = service.updateGeneralPastObstetricHistory( + { + value: { + pastObstericHistoryList: [ + { + durationType: { pregDurationID: 1, durationType: 'Term' }, + deliveryType: { deliveryTypeID: 2, deliveryType: 'Normal' }, + deliveryPlace: { deliveryPlaceID: 3, deliveryPlace: 'Home' }, + pregOutcome: { pregOutcomeID: 4, pregOutcome: 'Live' }, + newBornComplication: { + complicationID: 5, + complicationValue: 'None', + }, + }, + {}, + ], + }, + }, + { o: 1 } + ); + expect(res.pastObstericHistoryList).toBeUndefined(); + expect(res.femaleObstetricHistoryList[0]).toEqual({ + pregDurationID: 1, + durationType: 'Term', + deliveryTypeID: 2, + deliveryType: 'Normal', + deliveryPlaceID: 3, + deliveryPlace: 'Home', + pregOutcomeID: 4, + pregOutcome: 'Live', + newBornComplicationID: 5, + newBornComplication: 'None', + }); + }); + }); + + it('updatePNCDetails maps every PNC master field', () => { + service + .updatePNCDetails( + { + value: { + deliveryPlace: { deliveryPlaceID: 1, deliveryPlace: 'Home' }, + deliveryType: { deliveryTypeID: 2, deliveryType: 'Normal' }, + deliveryComplication: { + complicationID: 3, + deliveryComplicationType: 'PPH', + }, + pregOutcome: { pregOutcomeID: 4, pregOutcome: 'Live' }, + postNatalComplication: { + complicationID: 5, + complicationValue: 'Fever', + }, + gestationName: { gestationID: 6, name: 'Term' }, + newBornHealthStatus: { + newBornHealthStatusID: 7, + newBornHealthStatus: 'Good', + }, + }, + }, + { o: 1 } + ) + .subscribe(); + const req = httpMock.expectOne(environment.updatePNCDetailsUrl); + expect(req.request.body.PNCDetails).toEqual({ + deliveryPlaceID: 1, + deliveryPlace: 'Home', + deliveryTypeID: 2, + deliveryType: 'Normal', + deliveryComplicationID: 3, + deliveryComplication: 'PPH', + pregOutcomeID: 4, + pregOutcome: 'Live', + postNatalComplicationID: 5, + postNatalComplication: 'Fever', + gestationID: 6, + gestationName: 'Term', + newBornHealthStatusID: 7, + newBornHealthStatus: 'Good', + o: 1, + }); + req.flush({}); + }); + + it('postGeneralCaseRecordDiagnosis handles COVID and NCD screening categories', () => { + const form = { value: { d: 1 } }; + expect( + service.postGeneralCaseRecordDiagnosis(form, 'COVID-19 Screening', { + o: 1, + }) + ).toEqual({ d: 1, o: 1 }); + expect( + service.postGeneralCaseRecordDiagnosis(form, 'NCD screening', { o: 2 }) + ).toEqual({ d: 1, o: 2 }); + expect( + service.postGeneralCaseRecordDiagnosis(form, 'Other', {}) + ).toBeUndefined(); + }); + + it('saveSpecialistCancerObservation removes referredToInstituteName', () => { + const form = { + controls: { + patientCaseRecordForm: { value: { obs: 'o' } }, + patientReferForm: { + value: { referredToInstituteName: 'X', reason: 'r' }, + }, + }, + }; + service.saveSpecialistCancerObservation(form, { u: 1 }, true).subscribe(); + const req = httpMock.expectOne( + environment.saveSpecialistCancerObservationUrl + ); + expect(req.request.body.diagnosis).toEqual({ + reason: 'r', + obs: 'o', + u: 1, + doctorSignatureFlag: true, + }); + req.flush({}); + }); + + describe('postTMReferedNurseDetails', () => { + const tmForm = (value: any) => ({ + controls: { + patientVisitForm: new FormGroup({ + tmcConfirmationForm: new FormControl(value), + }), + }, + }); + + it('builds refer details from selected institution and uses prescribed drugs', () => { + service.prescribedDrugData = [{ drugID: 1 }]; + service + .postTMReferedNurseDetails( + tmForm({ + tmcConfirmed: true, + refrredToAdditionalServiceList: { + institutionID: 4, + institutionName: 'DH', + }, + }), + { createdBy: 'n' }, + null + ) + .subscribe(); + const req = httpMock.expectOne(environment.postNCDScreeningDetails); + const b = req.request.body; + expect(b.prescription).toEqual([{ drugID: 1 }]); + expect(b.isTMCDone).toBeTrue(); + expect(b.doctorFlag).toBe('1'); + expect(b.nurseFlag).toBe('2'); + expect(b.pharmacist_flag).toBe(0); + expect(b.benVisitID).toBe('111'); + expect(b.refer).toEqual( + jasmine.objectContaining({ + referredToInstituteID: 4, + referredToInstituteName: 'DH', + vanID: 10, + parkingPlaceID: 20, + beneficiaryID: '333', + benFlowID: '222', + benVisitID: '111', + createdBy: 'n', + isSpecialist: false, + }) + ); + req.flush({}); + }); + + it('sends null prescription and minimal refer when nothing selected', () => { + service.prescribedDrugData = null; + sessionStorageSpy.getItem.and.callFake((key: string) => + key === 'beneficiaryData' + ? JSON.stringify({ + doctorFlag: 1, + nurseFlag: 1, + pharmacist_flag: 1, + benVisitID: 5, + }) + : mockSessionStore[key] || null + ); + service + .postTMReferedNurseDetails(tmForm({ tmcConfirmed: false }), {}, null) + .subscribe(); + const req = httpMock.expectOne(environment.postNCDScreeningDetails); + expect(req.request.body.prescription).toBeNull(); + expect(req.request.body.pharmacist_flag).toBe(1); + expect(req.request.body.refer).toEqual({ benVisitID: '5' }); + req.flush({}); + }); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/services/idrsscore.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/idrsscore.service.spec.ts index fe69f5a7..73f8e0ce 100644 --- a/src/app/app-modules/nurse-doctor/shared/services/idrsscore.service.spec.ts +++ b/src/app/app-modules/nurse-doctor/shared/services/idrsscore.service.spec.ts @@ -20,21 +20,323 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { IdrsscoreService } from './idrsscore.service'; describe('IdrsscoreService', () => { + let service: IdrsscoreService; + beforeEach(() => { TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], providers: [IdrsscoreService], }); + service = TestBed.inject(IdrsscoreService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('listen and filter', () => { + it('should emit value through listen when filter is called', done => { + service.listen().subscribe(val => { + expect(val).toBe('test'); + done(); + }); + service.filter('test'); + }); + }); + + describe('setdymmyvalue', () => { + it('should emit disease through dummyValue$', done => { + service.dummyValue$.subscribe(val => { + if (val === 'diabetes') { + expect(val).toBe('diabetes'); + done(); + } + }); + service.setdymmyvalue('diabetes'); + }); + }); + + describe('IDRS Family Score', () => { + it('should set and emit family score', done => { + service.IDRSFamilyScore$.subscribe(val => { + if (val === 30) { + expect(val).toBe(30); + expect(service.IRDSscore).toBe(30); + done(); + } + }); + service.setIDRSFamilyScore(30); + }); }); - it('should be created', inject( - [IdrsscoreService], - (service: IdrsscoreService) => { - expect(service).toBeTruthy(); - } - )); + describe('setConfirmedDisease', () => { + it('should set and emit confirmed value', done => { + service.confirmed$.subscribe(val => { + if (val === false) { + expect(val).toBe(false); + done(); + } + }); + service.setConfirmedDisease(false); + }); + }); + + describe('clearMessage', () => { + it('should reset family, waist, and physical activity scores to 0', () => { + service.setIDRSFamilyScore(30); + service.setIDRSScoreWaist(20); + service.setIRDSscorePhysicalActivity(10); + service.clearMessage(); + + let familyScore: any; + service.IDRSFamilyScore$.subscribe(v => (familyScore = v)); + expect(familyScore).toBe(0); + + let waistScore: any; + service.IDRSWaistScore$.subscribe(v => (waistScore = v)); + expect(waistScore).toBe(0); + + let activityScore: any; + service.IDRSPhysicalActivityScore$.subscribe(v => (activityScore = v)); + expect(activityScore).toBe(0); + }); + }); + + describe('IDRS Waist Score', () => { + it('should set and emit waist score', done => { + service.IDRSWaistScore$.subscribe(val => { + if (val === 20) { + expect(service.IRDSscoreWaist).toBe(20); + done(); + } + }); + service.setIDRSScoreWaist(20); + }); + }); + + describe('IDRS Physical Activity Score', () => { + it('should set and emit physical activity score', done => { + service.IDRSPhysicalActivityScore$.subscribe(val => { + if (val === 15) { + expect(service.IRDSscorePhysicalActivity).toBe(15); + done(); + } + }); + service.setIRDSscorePhysicalActivity(15); + }); + }); + + describe('IDRS Score Flag', () => { + it('should set flag to 1', () => { + service.setIDRSScoreFlag(); + expect(service.IDRSScoreFlag).toBe(1); + let flagVal: any; + service.IDRSScoreFlagCheck$.subscribe(v => (flagVal = v)); + expect(flagVal).toBe(1); + }); + + it('should clear flag to 0', () => { + service.setIDRSScoreFlag(); + service.clearScoreFlag(); + expect(service.IDRSScoreFlag).toBe(0); + }); + }); + + describe('Suspected Array', () => { + it('should set suspected flag', () => { + service.setSuspectedArrayValue(); + expect(service.IDRSSuspected).toBe(1); + }); + + it('should clear suspected flag', () => { + service.setSuspectedArrayValue(); + service.clearSuspectedArrayFlag(); + expect(service.IDRSSuspected).toBe(0); + }); + }); + + describe('Diabetes Selected', () => { + it('should set diabetes selected', () => { + service.setDiabetesSelected(); + expect(service.diabetesSelected).toBe(1); + }); + + it('should clear diabetes selected', () => { + service.setDiabetesSelected(); + service.clearDiabetesSelected(); + expect(service.diabetesSelected).toBe(0); + }); + }); + + describe('Visual Acuity Test Mandatory', () => { + it('should set mandatory flag', () => { + service.setVisualAcuityTestMandatoryFlag(); + expect(service.VisualAcuityTestMandatory).toBe(1); + }); + + it('should clear mandatory flag', () => { + service.setVisualAcuityTestMandatoryFlag(); + service.clearVisualAcuityTestMandatoryFlag(); + expect(service.VisualAcuityTestMandatory).toBe(0); + }); + }); + + describe('Systolic BP', () => { + it('should set systolic bp', () => { + service.setSystolicBp(120); + expect(service.systolicBp).toBe(120); + }); + + it('should clear systolic bp to 0', () => { + service.setSystolicBp(120); + service.clearSystolicBp(); + expect(service.systolicBp).toBe(0); + }); + }); + + describe('Diastolic BP', () => { + it('should set diastolic bp', () => { + service.setDiastolicBp(80); + expect(service.diastolicBp).toBe(80); + }); + + it('should clear diastolic bp to 0', () => { + service.setDiastolicBp(80); + service.clearDiastolicBp(); + expect(service.diastolicBp).toBe(0); + }); + }); + + describe('Master Present Flags', () => { + it('should set rBS present flag', () => { + service.rBSPresentInMaster(); + expect(service.rBSPresent).toBe(1); + }); + + it('should set visual acuity present flag', () => { + service.visualAcuityPresentInMaster(); + expect(service.visualAcuityPresent).toBe(1); + }); + + it('should set haemoglobin present flag', () => { + service.haemoglobinPresentInMaster(); + expect(service.heamoglobinPresent).toBe(1); + }); + }); + + describe('TMC Suggested', () => { + it('should set TMC suggested', () => { + service.setTMCSuggested(); + expect(service.tmcSuggested).toBe(1); + }); + + it('should clear TMC suggested', () => { + service.setTMCSuggested(); + service.clearTMCSuggested(); + let val: any; + service.tmcSuggestedFlag$.subscribe(v => (val = v)); + expect(val).toBe(0); + }); + }); + + describe('Referral Suggested', () => { + it('should set referral suggested', () => { + service.setReferralSuggested(); + expect(service.referralSuggested).toBe(1); + }); + + it('should clear referral suggested', () => { + service.setReferralSuggested(); + service.clearReferralSuggested(); + expect(service.referralSuggested).toBe(0); + }); + }); + + describe('Diseases Selected/Unchecked', () => { + it('should set diseases selected', done => { + service.visitDiseases$.subscribe(val => { + if (val === 'Diabetes') { + expect(val).toBe('Diabetes'); + done(); + } + }); + service.setDiseasesSelected('Diabetes'); + }); + + it('should clear diseases selected', () => { + service.setDiseasesSelected('Diabetes'); + service.clearDiseaseSelected(); + let val: any; + service.visitDiseases$.subscribe(v => (val = v)); + expect(val).toBeNull(); + }); + + it('should set unchecked diseases', done => { + service.uncheckedDiseases$.subscribe(val => { + if (val === 'Hypertension') { + expect(val).toBe('Hypertension'); + done(); + } + }); + service.setUnchecked('Hypertension'); + }); + + it('should clear unchecked diseases', () => { + service.setUnchecked('Hypertension'); + service.clearUnchecked(); + let val: any; + service.uncheckedDiseases$.subscribe(v => (val = v)); + expect(val).toBeNull(); + }); + }); + + describe('TMC Submit Disable', () => { + it('should set TMC submit disable flag', () => { + service.setTMCSubmit(false); + let val: any; + service.tmcSubmitDisable$.subscribe(v => (val = v)); + expect(val).toBe(false); + }); + }); + + describe('Hypertension Selected', () => { + it('should set hypertension selected', () => { + service.setHypertensionSelected(); + expect(service.hypertensionSelected).toBe(1); + }); + + it('should clear hypertension selected', () => { + service.setHypertensionSelected(); + service.clearHypertensionSelected(); + expect(service.hypertensionSelected).toBe(0); + }); + }); + + describe('Confirmed Diabetic Selected', () => { + it('should set confirmed diabetic selected', () => { + service.setConfirmedDiabeticSelected(); + expect(service.confirmedDiabeticSelected).toBe(1); + }); + + it('should clear confirmed diabetic selected', () => { + service.setConfirmedDiabeticSelected(); + service.clearConfirmedDiabeticSelected(); + expect(service.confirmedDiabeticSelected).toBe(0); + }); + }); + + describe('boolean flags', () => { + it('should have isHypertensionConfirmed as false by default', () => { + expect(service.isHypertensionConfirmed).toBeFalse(); + }); + + it('should have isDiabeticsConfirmed as false by default', () => { + expect(service.isDiabeticsConfirmed).toBeFalse(); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/shared/services/masterdata.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/masterdata.service.spec.ts new file mode 100644 index 00000000..6c4633bf --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/services/masterdata.service.spec.ts @@ -0,0 +1,404 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { MasterdataService } from './masterdata.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; + +describe('MasterdataService', () => { + let service: MasterdataService; + let httpMock: HttpTestingController; + let sessionStorageSpy: jasmine.SpyObj; + + beforeEach(() => { + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + MasterdataService, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + ], + }); + + service = TestBed.inject(MasterdataService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('URL properties', () => { + it('should initialize URL properties from environment', () => { + expect(service.visitDetailMasterDataUrl).toBe( + environment.visitDetailMasterDataUrl + ); + expect(service.nurseMasterDataUrl).toBe(environment.nurseMasterDataUrl); + expect(service.doctorMasterDataUrl).toBe(environment.doctorMasterDataUrl); + expect(service.snoMedDataURL).toBe(environment.snomedCTRecordURL); + expect(service.diagnosisSnomedCTRecordUrl).toBe( + environment.diagnosisSnomedCTRecordUrl + ); + expect(service.diagnosisSnomedCTRecordUrl1).toBe( + environment.diagnosisSnomedCTRecordUrl1 + ); + expect(service.vaccinationTypeAndDoseMasterUrl).toBe( + environment.vaccinationTypeAndDoseMasterUrl + ); + expect(service.previousCovidVaccinationUrl).toBe( + environment.previousCovidVaccinationUrl + ); + expect(service.getCalibrationStrips).toBe( + environment.getCalibrationStrips + ); + }); + }); + + describe('BehaviorSubjects initial state', () => { + it('should have null initial value for visitDetailMasterDataSource', done => { + service.visitDetailMasterData$.subscribe(data => { + expect(data).toBeNull(); + done(); + }); + }); + + it('should have null initial value for nurseMasterDataSource', done => { + service.nurseMasterData$.subscribe(data => { + expect(data).toBeNull(); + done(); + }); + }); + + it('should have null initial value for doctorMasterDataSource', done => { + service.doctorMasterData$.subscribe(data => { + expect(data).toBeNull(); + done(); + }); + }); + }); + + describe('listen, filter, contactfilter', () => { + it('listen should return an Observable', () => { + const obs = service.listen(); + expect(obs.subscribe).toBeDefined(); + }); + + it('filter should emit the filterBy value through listen', done => { + service.listen().subscribe(value => { + expect(value).toBe('testFilter'); + done(); + }); + service.filter('testFilter'); + }); + + it('contactfilter should emit the filterBy value through listen', done => { + service.listen().subscribe(value => { + expect(value).toBe('contactValue'); + done(); + }); + service.contactfilter('contactValue'); + }); + }); + + describe('getVisitDetailMasterData', () => { + it('should make GET request and update visitDetailMasterDataSource', done => { + const mockData = { visitCategories: ['ANC', 'PNC'] }; + + service.getVisitDetailMasterData(); + + const req = httpMock.expectOne(environment.visitDetailMasterDataUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: mockData }); + + service.visitDetailMasterData$.subscribe(data => { + expect(data).toEqual(mockData); + done(); + }); + }); + }); + + describe('getNurseMasterData', () => { + it('should make GET request with visitID, providerServiceID, and gender and update nurseMasterDataSource', done => { + const mockData = { nurseMaster: 'data' }; + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'beneficiaryGender') return 'Male'; + return null; + }); + + service.getNurseMasterData('4', '10'); + + const expectedUrl = environment.nurseMasterDataUrl + '4/10/Male'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: mockData }); + + service.nurseMasterData$.subscribe(data => { + expect(data).toEqual(mockData); + done(); + }); + }); + }); + + describe('getDoctorMasterData', () => { + it('should make GET request with visitID, providerServiceID, gender, facilityID, and vanID and update doctorMasterDataSource', done => { + const mockData = { doctorMaster: 'data' }; + const serviceLineDetails = JSON.stringify({ + facilityID: 5, + vanID: 12, + }); + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return serviceLineDetails; + if (key === 'beneficiaryGender') return 'Female'; + return null; + }); + + service.getDoctorMasterData('3', '20'); + + const expectedUrl = environment.doctorMasterDataUrl + '3/20/Female/5/12'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: mockData }); + + service.doctorMasterData$.subscribe(data => { + expect(data).toEqual(mockData); + done(); + }); + }); + + it('should default facilityID and vanID to 0 when not present', done => { + const mockData = { doctorMaster: 'default' }; + const serviceLineDetails = JSON.stringify({}); + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return serviceLineDetails; + if (key === 'beneficiaryGender') return 'Male'; + return null; + }); + + service.getDoctorMasterData('1', '5'); + + const expectedUrl = environment.doctorMasterDataUrl + '1/5/Male/0/0'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: mockData }); + + service.doctorMasterData$.subscribe(data => { + expect(data).toEqual(mockData); + done(); + }); + }); + }); + + describe('getDoctorMasterDataForNurse', () => { + it('should return an Observable and not subscribe internally', () => { + const serviceLineDetails = JSON.stringify({ + facilityID: 2, + vanID: 8, + }); + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return serviceLineDetails; + if (key === 'beneficiaryGender') return 'Male'; + return null; + }); + + const result = service.getDoctorMasterDataForNurse('6', '15'); + expect(result.subscribe).toBeDefined(); + + result.subscribe((res: any) => { + expect(res).toEqual({ data: 'test' }); + }); + + const expectedUrl = environment.doctorMasterDataUrl + '6/15/Male/2/8'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: 'test' }); + }); + }); + + describe('getSnomedCTRecord', () => { + it('should make POST request with term', () => { + service.getSnomedCTRecord('fever').subscribe((res: any) => { + expect(res).toEqual({ result: 'snomed' }); + }); + + const req = httpMock.expectOne(environment.snomedCTRecordURL); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ term: 'fever' }); + req.flush({ result: 'snomed' }); + }); + }); + + describe('getReportsMaster', () => { + it('should make GET request with serviceID from session', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'serviceID') return '100'; + return null; + }); + + service.getReportsMaster().subscribe((res: any) => { + expect(res).toEqual({ reports: [] }); + }); + + const req = httpMock.expectOne(environment.getReportsMasterUrl + '100'); + expect(req.request.method).toBe('GET'); + req.flush({ reports: [] }); + }); + }); + + describe('getVanMaster', () => { + it('should make GET request with providerServiceID from session', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === 'providerServiceID') return '50'; + return null; + }); + + service.getVanMaster().subscribe((res: any) => { + expect(res).toEqual({ vans: [] }); + }); + + const req = httpMock.expectOne(environment.getVanMasterUrl + '50'); + expect(req.request.method).toBe('GET'); + req.flush({ vans: [] }); + }); + }); + + describe('getReportData', () => { + it('should make POST request with report request payload', () => { + const reportRequest = { reportType: 'daily', date: '2024-01-01' }; + + service.getReportData(reportRequest).subscribe((res: any) => { + expect(res).toEqual({ report: 'data' }); + }); + + const req = httpMock.expectOne(environment.getReportDataUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(reportRequest); + req.flush({ report: 'data' }); + }); + }); + + describe('searchDiagnosisBasedOnPageNo', () => { + it('should make POST request with searchTerm and pageNo', () => { + service + .searchDiagnosisBasedOnPageNo('diabetes', 1) + .subscribe((res: any) => { + expect(res).toEqual({ results: [] }); + }); + + const req = httpMock.expectOne(environment.diagnosisSnomedCTRecordUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ term: 'diabetes', pageNo: 1 }); + req.flush({ results: [] }); + }); + }); + + describe('searchDiagnosisBasedOnPageNo1', () => { + it('should make POST request with searchTerm and pageNo', () => { + service + .searchDiagnosisBasedOnPageNo1('asthma', 2) + .subscribe((res: any) => { + expect(res).toEqual({ results: ['asthma'] }); + }); + + const req = httpMock.expectOne(environment.diagnosisSnomedCTRecordUrl1); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ term: 'asthma', pageNo: 2 }); + req.flush({ results: ['asthma'] }); + }); + }); + + describe('fetchCalibrationStrips', () => { + it('should make POST request with providerServiceMapID and pageNo', () => { + service.fetchCalibrationStrips('25', 0).subscribe((res: any) => { + expect(res).toEqual({ strips: [] }); + }); + + const req = httpMock.expectOne(environment.getCalibrationStrips); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + providerServiceMapID: '25', + pageNo: 0, + }); + req.flush({ strips: [] }); + }); + }); + + describe('getVaccinationTypeAndDoseMaster', () => { + it('should make GET request', () => { + service.getVaccinationTypeAndDoseMaster().subscribe((res: any) => { + expect(res).toEqual({ types: [] }); + }); + + const req = httpMock.expectOne( + environment.vaccinationTypeAndDoseMasterUrl + ); + expect(req.request.method).toBe('GET'); + req.flush({ types: [] }); + }); + }); + + describe('getPreviousCovidVaccinationDetails', () => { + it('should make POST request with beneficiaryRegID', () => { + service + .getPreviousCovidVaccinationDetails('12345') + .subscribe((res: any) => { + expect(res).toEqual({ vaccinations: [] }); + }); + + const req = httpMock.expectOne(environment.previousCovidVaccinationUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ beneficiaryRegID: '12345' }); + req.flush({ vaccinations: [] }); + }); + }); + + describe('reset', () => { + it('should set all BehaviorSubjects to null', () => { + service.visitDetailMasterDataSource.next({ some: 'data' }); + service.nurseMasterDataSource.next({ nurse: 'data' }); + service.doctorMasterDataSource.next({ doctor: 'data' }); + + service.reset(); + + service.visitDetailMasterData$.subscribe(data => { + expect(data).toBeNull(); + }); + service.nurseMasterData$.subscribe(data => { + expect(data).toBeNull(); + }); + service.doctorMasterData$.subscribe(data => { + expect(data).toBeNull(); + }); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/services/nurse.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/nurse.service.spec.ts new file mode 100644 index 00000000..e6af0691 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/services/nurse.service.spec.ts @@ -0,0 +1,2310 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { NurseService } from './nurse.service'; +import { SpinnerService } from '../../../core/services/spinner.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; +import { FormControl, FormGroup, FormArray } from '@angular/forms'; + +describe('NurseService', () => { + let service: NurseService; + let httpMock: HttpTestingController; + let sessionStorageSpy: jasmine.SpyObj; + let spinnerSpy: jasmine.SpyObj; + + beforeEach(() => { + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + sessionStorageSpy.getItem.and.callFake((key: string) => { + const store: Record = { + providerServiceID: '1234', + serviceID: '5', + serviceLineDetails: JSON.stringify({ vanID: 10, parkingPlaceID: 20 }), + beneficiaryRegID: '9876', + visitID: '111', + benFlowID: '222', + beneficiaryID: '333', + sessionID: 'sess1', + userName: 'testUser', + beneficiaryGender: 'Male', + }; + return store[key] || null; + }); + + spinnerSpy = jasmine.createSpyObj('SpinnerService', ['setLoading']); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + NurseService, + { provide: SpinnerService, useValue: spinnerSpy }, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + ], + }); + + service = TestBed.inject(NurseService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + // ------------------------------------------------------------------- + // Initial property values + // ------------------------------------------------------------------- + describe('initial state', () => { + it('should have temp set to false', () => { + expect(service.temp).toBeFalse(); + }); + + it('should have rbsSelectedInvestigation set to false', () => { + expect(service.rbsSelectedInvestigation).toBeFalse(); + }); + + it('should have rbsCurrentTestResult set to null', () => { + expect(service.rbsCurrentTestResult).toBeNull(); + }); + + it('should have isAssessmentDone set to false', () => { + expect(service.isAssessmentDone).toBeFalse(); + }); + }); + + // ------------------------------------------------------------------- + // Subject / BehaviorSubject emissions + // ------------------------------------------------------------------- + describe('BehaviorSubject - ncdTemp', () => { + it('should emit initial value false', (done: DoneFn) => { + service.ncdTemp$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setNCDTemp should emit the given score', (done: DoneFn) => { + service.setNCDTemp(true); + service.ncdTemp$.subscribe(value => { + expect(value).toBeTrue(); + done(); + }); + }); + + it('clearMessage should reset ncdTemp to false', (done: DoneFn) => { + service.setNCDTemp(true); + service.clearMessage(); + service.ncdTemp$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setNCDTemp should also update temp property', () => { + service.setNCDTemp(true); + expect(service.temp).toBeTrue(); + }); + + it('clearMessage should reset temp property to false', () => { + service.setNCDTemp(true); + service.clearMessage(); + expect(service.temp).toBeFalse(); + }); + }); + + describe('BehaviorSubject - rbsSelectedInInvestigation', () => { + it('should emit initial value false', (done: DoneFn) => { + service.rbsSelectedInInvestigation$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setRbsSelectedInInvestigation should emit the given score', (done: DoneFn) => { + service.setRbsSelectedInInvestigation(true); + service.rbsSelectedInInvestigation$.subscribe(value => { + expect(value).toBeTrue(); + done(); + }); + }); + + it('clearRbsSelectedInInvestigation should reset to false', (done: DoneFn) => { + service.setRbsSelectedInInvestigation(true); + service.clearRbsSelectedInInvestigation(); + service.rbsSelectedInInvestigation$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + }); + + describe('BehaviorSubject - rbsTestResultCurrent', () => { + it('should emit initial value null', (done: DoneFn) => { + service.rbsTestResultCurrent$.subscribe(value => { + expect(value).toBeNull(); + done(); + }); + }); + + it('setRbsInCurrentVitals should emit the given score', (done: DoneFn) => { + service.setRbsInCurrentVitals(42); + service.rbsTestResultCurrent$.subscribe(value => { + expect(value).toBe(42); + done(); + }); + }); + + it('clearRbsInVitals should reset to null', (done: DoneFn) => { + service.setRbsInCurrentVitals(42); + service.clearRbsInVitals(); + service.rbsTestResultCurrent$.subscribe(value => { + expect(value).toBeNull(); + done(); + }); + }); + + it('setRbsInCurrentVitals should also update rbsCurrentTestResult property', () => { + service.setRbsInCurrentVitals('testResult'); + expect(service.rbsCurrentTestResult).toBe('testResult'); + }); + }); + + describe('BehaviorSubject - enableLAssessment', () => { + it('should emit initial value false', (done: DoneFn) => { + service.enableLAssessment$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setEnableLAssessment should emit the given score', (done: DoneFn) => { + service.setEnableLAssessment(true); + service.enableLAssessment$.subscribe(value => { + expect(value).toBeTrue(); + done(); + }); + }); + + it('clearEnableLAssessment should reset to false', (done: DoneFn) => { + service.setEnableLAssessment(true); + service.clearEnableLAssessment(); + service.enableLAssessment$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setEnableLAssessment should also update temp property', () => { + service.setEnableLAssessment(true); + expect(service.temp).toBeTrue(); + }); + }); + + describe('BehaviorSubject - enableProvisionalDiag', () => { + it('should emit initial value false', (done: DoneFn) => { + service.enableProvisionalDiag$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setNCDScreeningProvision should emit the given score', (done: DoneFn) => { + service.setNCDScreeningProvision(true); + service.enableProvisionalDiag$.subscribe(value => { + expect(value).toBeTrue(); + done(); + }); + }); + + it('clearNCDScreeningProvision should reset to false', (done: DoneFn) => { + service.setNCDScreeningProvision(true); + service.clearNCDScreeningProvision(); + service.enableProvisionalDiag$.subscribe(value => { + expect(value).toBeFalse(); + done(); + }); + }); + + it('setNCDScreeningProvision should also update temp property', () => { + service.setNCDScreeningProvision(true); + expect(service.temp).toBeTrue(); + }); + }); + + // ------------------------------------------------------------------- + // Subject - listen / filter + // ------------------------------------------------------------------- + describe('listen / filter', () => { + it('listen should return an observable that emits when filter is called', (done: DoneFn) => { + service.listen().subscribe(value => { + expect(value).toBe('myFilter'); + done(); + }); + service.filter('myFilter'); + }); + }); + + // ------------------------------------------------------------------- + // Worklist methods (GET) + // ------------------------------------------------------------------- + describe('worklist methods', () => { + it('getNurseWorklist should call GET with correct URL', () => { + service.getNurseWorklist().subscribe(); + const expectedUrl = environment.nurseWorklist + '1234/5/10'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: 'worklist' }); + }); + + it('getNurseWorklistTMreferred should call GET with correct URL', () => { + service.getNurseWorklistTMreferred().subscribe(); + const expectedUrl = environment.nurseWorklistTMreferred + '1234/5/10'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ data: 'tmWorklist' }); + }); + }); + + // ------------------------------------------------------------------- + // Previous history GET/POST methods + // ------------------------------------------------------------------- + describe('previous history methods', () => { + it('getPreviousDiabetesHistory should POST benRegID', () => { + service.getPreviousDiabetesHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousDiabetesHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousVisitData should POST the given obj', () => { + const obj = { someKey: 'someValue' }; + service.getPreviousVisitData(obj).subscribe(); + const req = httpMock.expectOne(environment.previousVisitDataUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(obj); + req.flush({}); + }); + + it('getPreviousCancerFamilyHistory should POST benRegID', () => { + service.getPreviousCancerFamilyHistory('12345').subscribe(); + const req = httpMock.expectOne( + environment.previousCancerFamilyHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousCancerPersonalHabitHistory should POST benRegID', () => { + service.getPreviousCancerPersonalHabitHistory('12345').subscribe(); + const req = httpMock.expectOne( + environment.previousCancerPersonalHabitHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousCancerPersonalDietHistory should POST benRegID', () => { + service.getPreviousCancerPersonalDietHistory('12345').subscribe(); + const req = httpMock.expectOne( + environment.previousCancerPersonalDietHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousCancerPastObstetricHistory should POST benRegID', () => { + service.getPreviousCancerPastObstetricHistory('12345').subscribe(); + const req = httpMock.expectOne( + environment.previousCancerPastObstetricHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousPastHistory should POST benRegID', () => { + service.getPreviousPastHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousPastHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousMedicationHistory should POST benRegID', () => { + service.getPreviousMedicationHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousMedicationHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousOtherVaccines should POST benRegID', () => { + service.getPreviousOtherVaccines('12345', 'ANC').subscribe(); + const req = httpMock.expectOne( + environment.previousOtherVaccineHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousTobaccoHistory should POST benRegID', () => { + service.getPreviousTobaccoHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousTobaccoHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousAlcoholHistory should POST benRegID', () => { + service.getPreviousAlcoholHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousAlcoholHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousAllergyHistory should POST benRegID', () => { + service.getPreviousAllergyHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousAllergyHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousFamilyHistory should POST benRegID', () => { + service.getPreviousFamilyHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousFamilyHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousMenstrualHistory should POST benRegID', () => { + service.getPreviousMenstrualHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousMestrualHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousObstetricHistory should POST benRegID', () => { + service.getPreviousObstetricHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne( + environment.previousPastObstetricHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousComorbidityHistory should POST benRegID', () => { + service.getPreviousComorbidityHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousComorbidityHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousDevelopmentalHistory should POST benRegID', () => { + service.getPreviousDevelopmentalHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousDevelopmentHistory); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousPerinatalHistory should POST benRegID', () => { + service.getPreviousPerinatalHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousPerinatalHistory); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousFeedingHistory should POST benRegID', () => { + service.getPreviousFeedingHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousFeedingHistory); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousImmunizationHistory should POST benRegID', () => { + service.getPreviousImmunizationHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne( + environment.previousImmunizationHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousPhysicalActivityHistory should POST benRegID', () => { + service.getPreviousPhysicalActivityHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne( + environment.previousPhyscialactivityHistoryUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + + it('getPreviousReferredHistory should POST benRegID', () => { + service.getPreviousReferredHistory('12345', 'ANC').subscribe(); + const req = httpMock.expectOne(environment.previousReferredHistoryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benRegID: '12345' }); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // Location methods (GET) + // ------------------------------------------------------------------- + describe('location methods', () => { + it('getCountryName should call GET', () => { + service.getCountryName().subscribe(); + const req = httpMock.expectOne(environment.getCountryName); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + + it('getCityName should call GET with countryID appended', () => { + service.getCityName(99).subscribe(); + const req = httpMock.expectOne(environment.getCityName + '99/'); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + + it('getStateName should call GET with value appended', () => { + service.getStateName(5).subscribe(); + const req = httpMock.expectOne(environment.getStateName + '5'); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + + it('getDistrictName should call GET with stateID appended', () => { + service.getDistrictName(3).subscribe(); + const req = httpMock.expectOne(environment.getDistrictName + '3'); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + + it('getSubDistrictName should call GET with districtID appended', () => { + service.getSubDistrictName(7).subscribe(); + const req = httpMock.expectOne(environment.getSubDistrictName + '7'); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // NCD screening visit count & misc GET/POST + // ------------------------------------------------------------------- + describe('NCD screening visit count and miscellaneous', () => { + it('getNcdScreeningVisitCount should call GET with beneficiaryRegID', () => { + service.getNcdScreeningVisitCount('9876').subscribe(); + const req = httpMock.expectOne( + environment.getNcdScreeningVisitCountUrl + '9876' + ); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + + it('getTMReferredCasesheetData should POST the given object', () => { + const reqObj = { benFlowID: '222' }; + service.getTMReferredCasesheetData(reqObj).subscribe(); + const req = httpMock.expectOne(environment.getTMCasesheetData); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(reqObj); + req.flush({}); + }); + + it('calculateBmiStatus should POST the given object', () => { + const obj = { height: 170, weight: 70 }; + service.calculateBmiStatus(obj).subscribe(); + const req = httpMock.expectOne(environment.calculateBmiStatus); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(obj); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // Helper: build a mock "generic" history form for reuse + // ------------------------------------------------------------------- + function buildMockHistorySubForm(value: any) { + return { + value: value, + getRawValue: () => value, + controls: {}, + }; + } + + function buildMockHistoryControls() { + return { + controls: { + pastHistory: buildMockHistorySubForm({ + pastIllness: [], + pastSurgery: [], + }), + comorbidityHistory: buildMockHistorySubForm({ + comorbidityConcurrentConditionsList: [], + }), + medicationHistory: buildMockHistorySubForm({}), + pastObstericHistory: buildMockHistorySubForm({ + pastObstericHistoryList: [], + }), + menstrualHistory: { + value: { + menstrualCycleStatus: null, + cycleLength: null, + bloodFlowDuration: null, + lMPDate: null, + }, + getRawValue() { + return this.value; + }, + controls: {}, + }, + familyHistory: buildMockHistorySubForm({ familyDiseaseList: [] }), + personalHistory: { + value: { + tobaccoList: [], + alcoholList: [], + allergicList: [], + riskySexualPracticesStatus: null, + }, + getRawValue() { + return this.value; + }, + controls: {}, + }, + otherVaccines: buildMockHistorySubForm({ otherVaccines: [] }), + immunizationHistory: buildMockHistorySubForm({ immunizationList: [] }), + developmentHistory: buildMockHistorySubForm({}), + feedingHistory: buildMockHistorySubForm({ foodIntoleranceStatus: '0' }), + perinatalHistory: buildMockHistorySubForm({ + placeOfDelivery: null, + typeOfDelivery: null, + complicationAtBirth: null, + }), + physicalActivityHistory: buildMockHistorySubForm({}), + }, + }; + } + + function buildMockVisitForm() { + return { + controls: { + patientVisitDetailsForm: { + value: { visitReason: 'New Chief Complaint' }, + }, + patientFileUploadDetailsForm: { value: { fileIDs: [] } }, + patientChiefComplaintsForm: { value: { complaints: [] } }, + patientAdherenceForm: { value: {} }, + patientInvestigationsForm: { value: {} }, + patientCovidForm: { value: {} }, + }, + }; + } + + function buildMockExaminationFormValue() { + return { + generalExaminationForm: {}, + headToToeExaminationForm: {}, + systemicExaminationForm: { + gastroIntestinalSystemForm: {}, + cardioVascularSystemForm: {}, + respiratorySystemForm: {}, + centralNervousSystemForm: {}, + musculoSkeletalSystemForm: {}, + genitoUrinarySystemForm: {}, + obstetricExaminationForANCForm: {}, + }, + }; + } + + function buildMockVitalsForm() { + return { value: { height: 170, weight: 70 } }; + } + + // ------------------------------------------------------------------- + // postPatientVisitDetails + // ------------------------------------------------------------------- + describe('postPatientVisitDetails', () => { + it('should return merged object with session data', () => { + const result = service.postPatientVisitDetails( + { visitReason: 'Follow Up' }, + { fileIDs: [1, 2] } + ); + expect(result.visitReason).toBe('Follow Up'); + expect(result.fileIDs).toEqual([1, 2]); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.providerServiceMapID).toBe('1234'); + expect(result.createdBy).toBe('testUser'); + }); + }); + + // ------------------------------------------------------------------- + // postCheifComplaintForm + // ------------------------------------------------------------------- + describe('postCheifComplaintForm', () => { + it('should process complaints and attach session data', () => { + const complaints = [ + { + chiefComplaint: { + chiefComplaintID: 100, + chiefComplaint: 'Headache', + }, + }, + ]; + const result = service.postCheifComplaintForm(complaints, 'visit1'); + expect(result[0].chiefComplaintID).toBe(100); + expect(result[0].chiefComplaint).toBe('Headache'); + expect(result[0].beneficiaryRegID).toBe('9876'); + expect(result[0].benVisitID).toBe('visit1'); + expect(result[0].providerServiceMapID).toBe('1234'); + expect(result[0].createdBy).toBe('testUser'); + }); + + it('should handle null chiefComplaint gracefully', () => { + const complaints = [{ chiefComplaint: null }]; + const result = service.postCheifComplaintForm(complaints, 'visit1'); + expect(result[0].beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postAdherenceForm + // ------------------------------------------------------------------- + describe('postAdherenceForm', () => { + it('should merge adherence data with session data', () => { + const result = service.postAdherenceForm({ toDrugs: true }, 'visit1'); + expect(result.toDrugs).toBeTrue(); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe('visit1'); + }); + }); + + // ------------------------------------------------------------------- + // postInvestigationForm + // ------------------------------------------------------------------- + describe('postInvestigationForm', () => { + it('should merge investigation data with session data', () => { + const result = service.postInvestigationForm( + { labTest: 'CBC' }, + 'visit1' + ); + expect(result.labTest).toBe('CBC'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postCovidForm + // ------------------------------------------------------------------- + describe('postCovidForm', () => { + it('should merge covid form data with session data', () => { + const result = service.postCovidForm({ symptom: 'Cough' }, 'visit1'); + expect(result.symptom).toBe('Cough'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postGenericVitalForm + // ------------------------------------------------------------------- + describe('postGenericVitalForm', () => { + it('should merge vitals with session data', () => { + const result = service.postGenericVitalForm( + { value: { height: 170, weight: 70 } }, + 'visit1' + ); + expect(result.height).toBe(170); + expect(result.weight).toBe(70); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe('visit1'); + }); + }); + + // ------------------------------------------------------------------- + // Examination sub-form posting methods + // ------------------------------------------------------------------- + describe('examination sub-form posting', () => { + const formData = { finding: 'Normal' }; + const benVisitID = 'visit1'; + + it('postGeneralExaminationForm should merge with session data', () => { + const result = service.postGeneralExaminationForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe(benVisitID); + expect(result.providerServiceMapID).toBe('1234'); + expect(result.createdBy).toBe('testUser'); + }); + + it('postHeadToToeExaminationForm should merge with session data', () => { + const result = service.postHeadToToeExaminationForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postGastroIntestinalSystemForm should merge with session data', () => { + const result = service.postGastroIntestinalSystemForm( + formData, + benVisitID + ); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postCardioVascularSystemForm should merge with session data', () => { + const result = service.postCardioVascularSystemForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postRespiratorySystemForm should merge with session data', () => { + const result = service.postRespiratorySystemForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postCentralNervousSystemForm should merge with session data', () => { + const result = service.postCentralNervousSystemForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postMusculoSkeletalSystemForm should merge with session data', () => { + const result = service.postMusculoSkeletalSystemForm( + formData, + benVisitID + ); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postGenitoUrinarySystemForm should merge with session data', () => { + const result = service.postGenitoUrinarySystemForm(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('postANCObstetricExamination should merge with session data', () => { + const result = service.postANCObstetricExamination(formData, benVisitID); + expect(result.finding).toBe('Normal'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postGenericVisitDetailForm - visit category branching + // ------------------------------------------------------------------- + describe('postGenericVisitDetailForm', () => { + let mockVisitForm: any; + + beforeEach(() => { + mockVisitForm = buildMockVisitForm(); + }); + + it('should return ANC visit details with adherence and investigation for ANC', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'ANC' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.chiefComplaints).toBeDefined(); + expect(result.adherence).toBeDefined(); + expect(result.investigation).toBeDefined(); + }); + + it('should return General OPD visit details with chiefComplaints only', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'General OPD' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.chiefComplaints).toBeDefined(); + expect(result.adherence).toBeUndefined(); + expect(result.investigation).toBeUndefined(); + }); + + it('should return PNC visit details with chiefComplaints', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'PNC' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.chiefComplaints).toBeDefined(); + }); + + it('should return NCD care visit details with adherence and investigation', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'NCD care' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.adherence).toBeDefined(); + expect(result.investigation).toBeDefined(); + expect(result.chiefComplaints).toBeUndefined(); + }); + + it('should return COVID-19 Screening visit details with covidDetails', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'COVID-19 Screening' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.covidDetails).toBeDefined(); + expect(result.chiefComplaints).toBeUndefined(); + }); + + it('should return NCD screening visit details with chiefComplaints', () => { + const result: any = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'NCD screening' + ); + expect(result.visitDetails).toBeDefined(); + expect(result.chiefComplaints).toBeDefined(); + }); + + it('should return an Observable that completes for unknown visit category', (done: DoneFn) => { + const result = service.postGenericVisitDetailForm( + mockVisitForm, + 'visit1', + 'Unknown Category' + ); + result.subscribe({ + complete: () => { + done(); + }, + }); + }); + }); + + // ------------------------------------------------------------------- + // postGenericExaminationForm - visit category branching + // ------------------------------------------------------------------- + describe('postGenericExaminationForm', () => { + let mockExamFormValue: any; + + beforeEach(() => { + mockExamFormValue = buildMockExaminationFormValue(); + }); + + it('should include obstetricExamination for ANC', () => { + const result: any = service.postGenericExaminationForm( + mockExamFormValue, + 'visit1', + 'ANC' + ); + expect(result.generalExamination).toBeDefined(); + expect(result.headToToeExamination).toBeDefined(); + expect(result.cardioVascularExamination).toBeDefined(); + expect(result.respiratorySystemExamination).toBeDefined(); + expect(result.centralNervousSystemExamination).toBeDefined(); + expect(result.musculoskeletalSystemExamination).toBeDefined(); + expect(result.genitoUrinarySystemExamination).toBeDefined(); + expect(result.obstetricExamination).toBeDefined(); + // ANC does NOT include gastroIntestinalExamination + expect(result.gastroIntestinalExamination).toBeUndefined(); + }); + + it('should include gastroIntestinalExamination for General OPD but not obstetric', () => { + const result: any = service.postGenericExaminationForm( + mockExamFormValue, + 'visit1', + 'General OPD' + ); + expect(result.generalExamination).toBeDefined(); + expect(result.headToToeExamination).toBeDefined(); + expect(result.gastroIntestinalExamination).toBeDefined(); + expect(result.cardioVascularExamination).toBeDefined(); + expect(result.respiratorySystemExamination).toBeDefined(); + expect(result.centralNervousSystemExamination).toBeDefined(); + expect(result.musculoskeletalSystemExamination).toBeDefined(); + expect(result.genitoUrinarySystemExamination).toBeDefined(); + expect(result.obstetricExamination).toBeUndefined(); + }); + + it('should include gastroIntestinalExamination for PNC but not obstetric', () => { + const result: any = service.postGenericExaminationForm( + mockExamFormValue, + 'visit1', + 'PNC' + ); + expect(result.generalExamination).toBeDefined(); + expect(result.gastroIntestinalExamination).toBeDefined(); + expect(result.obstetricExamination).toBeUndefined(); + }); + + it('should return an Observable that completes for unknown visit category', (done: DoneFn) => { + const result = service.postGenericExaminationForm( + mockExamFormValue, + 'visit1', + 'Unknown Category' + ); + result.subscribe({ + complete: () => { + done(); + }, + }); + }); + }); + + // ------------------------------------------------------------------- + // postANCHistoryForm - benAge branching + // ------------------------------------------------------------------- + describe('postANCHistoryForm', () => { + let mockHistoryForm: any; + + beforeEach(() => { + mockHistoryForm = buildMockHistoryControls(); + }); + + it('should include childVaccineDetails and immunizationHistory when benAge <= 16', () => { + const result: any = service.postANCHistoryForm( + mockHistoryForm, + 'visit1', + 10 + ); + expect(result.pastHistory).toBeDefined(); + expect(result.comorbidConditions).toBeDefined(); + expect(result.medicationHistory).toBeDefined(); + expect(result.femaleObstetricHistory).toBeDefined(); + expect(result.menstrualHistory).toBeDefined(); + expect(result.familyHistory).toBeDefined(); + expect(result.personalHistory).toBeDefined(); + expect(result.childVaccineDetails).toBeDefined(); + expect(result.immunizationHistory).toBeDefined(); + }); + + it('should include childVaccineDetails and immunizationHistory when benAge is exactly 16', () => { + const result: any = service.postANCHistoryForm( + mockHistoryForm, + 'visit1', + 16 + ); + expect(result.childVaccineDetails).toBeDefined(); + expect(result.immunizationHistory).toBeDefined(); + }); + + it('should NOT include childVaccineDetails or immunizationHistory when benAge > 16', () => { + const result: any = service.postANCHistoryForm( + mockHistoryForm, + 'visit1', + 25 + ); + expect(result.pastHistory).toBeDefined(); + expect(result.comorbidConditions).toBeDefined(); + expect(result.medicationHistory).toBeDefined(); + expect(result.femaleObstetricHistory).toBeDefined(); + expect(result.menstrualHistory).toBeDefined(); + expect(result.familyHistory).toBeDefined(); + expect(result.personalHistory).toBeDefined(); + expect(result.childVaccineDetails).toBeUndefined(); + expect(result.immunizationHistory).toBeUndefined(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralHistoryForm + // ------------------------------------------------------------------- + describe('postGeneralHistoryForm', () => { + it('should return all history sections including development, feeding, perinatal', () => { + const mockHistoryForm = buildMockHistoryControls(); + const result: any = service.postGeneralHistoryForm(mockHistoryForm, { + ageVal: 10, + }); + expect(result.pastHistory).toBeDefined(); + expect(result.comorbidConditions).toBeDefined(); + expect(result.medicationHistory).toBeDefined(); + expect(result.femaleObstetricHistory).toBeDefined(); + expect(result.menstrualHistory).toBeDefined(); + expect(result.familyHistory).toBeDefined(); + expect(result.personalHistory).toBeDefined(); + expect(result.childVaccineDetails).toBeDefined(); + expect(result.immunizationHistory).toBeDefined(); + expect(result.developmentHistory).toBeDefined(); + expect(result.feedingHistory).toBeDefined(); + expect(result.perinatalHistroy).toBeDefined(); + }); + }); + + // ------------------------------------------------------------------- + // postNCDScreeningHistoryForm + // ------------------------------------------------------------------- + describe('postNCDScreeningHistoryForm', () => { + it('should return familyHistory, physicalActivityHistory, and personalHistory', () => { + const mockForm = buildMockHistoryControls(); + const result: any = service.postNCDScreeningHistoryForm( + mockForm, + null, + 'NCD screening' + ); + expect(result.familyHistory).toBeDefined(); + expect(result.physicalActivityHistory).toBeDefined(); + expect(result.personalHistory).toBeDefined(); + }); + }); + + // ------------------------------------------------------------------- + // postPhyscialActivityHistory + // ------------------------------------------------------------------- + describe('postPhyscialActivityHistory', () => { + it('should merge physical activity data with otherDetails', () => { + const result = service.postPhyscialActivityHistory( + { value: { activityType: 'Walking' } }, + { beneficiaryRegID: '9876' } + ); + expect(result.activityType).toBe('Walking'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralPastHistory + // ------------------------------------------------------------------- + describe('postGeneralPastHistory', () => { + it('should process illness and surgery types', () => { + const pastHistoryForm = { + value: { + pastIllness: [ + { + illnessType: { illnessType: 'Diabetes', illnessID: 1 }, + }, + ], + pastSurgery: [ + { + surgeryType: { surgeryType: 'Appendectomy', surgeryID: 2 }, + }, + ], + }, + }; + const otherDetails = { beneficiaryRegID: '9876' }; + const result = service.postGeneralPastHistory( + pastHistoryForm, + otherDetails + ); + expect(result.pastIllness[0].illnessType).toBe('Diabetes'); + expect(result.pastIllness[0].illnessTypeID).toBe('1'); + expect(result.pastSurgery[0].surgeryType).toBe('Appendectomy'); + expect(result.pastSurgery[0].surgeryID).toBe('2'); + expect(result.beneficiaryRegID).toBe('9876'); + }); + + it('should handle null illnessType', () => { + const pastHistoryForm = { + value: { + pastIllness: [{ illnessType: null }], + pastSurgery: [], + }, + }; + const result = service.postGeneralPastHistory(pastHistoryForm, {}); + expect(result.pastIllness[0].illnessType).toBeNull(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralComorbidityHistory + // ------------------------------------------------------------------- + describe('postGeneralComorbidityHistory', () => { + it('should process comorbid conditions', () => { + const form = { + value: { + comorbidityConcurrentConditionsList: [ + { + comorbidConditions: { + comorbidCondition: 'Hypertension', + comorbidConditionID: 10, + isForHistory: false, + }, + }, + ], + }, + }; + const result = service.postGeneralComorbidityHistory(form, { + beneficiaryRegID: '9876', + }); + expect( + result.comorbidityConcurrentConditionsList[0].comorbidCondition + ).toBe('Hypertension'); + expect( + result.comorbidityConcurrentConditionsList[0].comorbidConditionID + ).toBe('10'); + expect( + result.comorbidityConcurrentConditionsList[0].isForHistory + ).toBeTrue(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralFamilyHistory + // ------------------------------------------------------------------- + describe('postGeneralFamilyHistory', () => { + it('should process disease types', () => { + const form = { + value: { + familyDiseaseList: [ + { + diseaseType: { + diseaseType: 'Cancer', + diseaseTypeID: 5, + }, + }, + ], + }, + }; + const result = service.postGeneralFamilyHistory(form, {}); + expect(result.familyDiseaseList[0].diseaseType).toBe('Cancer'); + expect(result.familyDiseaseList[0].diseaseTypeID).toBe('5'); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralFeedingHistory + // ------------------------------------------------------------------- + describe('postGeneralFeedingHistory', () => { + it('should convert foodIntoleranceStatus to number', () => { + const form = { + value: { foodIntoleranceStatus: '1', typeOfFood: 'Milk' }, + }; + const result = service.postGeneralFeedingHistory(form, {}); + expect(result.foodIntoleranceStatus).toBe(1); + expect(result.typeOfFood).toBe('Milk'); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralMenstrualHistory + // ------------------------------------------------------------------- + describe('postGeneralMenstrualHistory', () => { + it('should process menstrual cycle status, cycleLength and bloodFlowDuration', () => { + const form = { + value: { + menstrualCycleStatus: { + menstrualCycleStatusID: 1, + name: 'Regular', + }, + cycleLength: { + menstrualRangeID: 2, + menstrualCycleRange: '24-28 days', + }, + bloodFlowDuration: { + menstrualRangeID: 3, + menstrualCycleRange: '3-5 days', + }, + lMPDate: null, + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralMenstrualHistory(form, {}); + expect(result.menstrualCycleStatusID).toBe('1'); + expect(result.menstrualCycleStatus).toBe('Regular'); + expect(result.menstrualCyclelengthID).toBe('2'); + expect(result.cycleLength).toBe('24-28 days'); + expect(result.menstrualFlowDurationID).toBe('3'); + expect(result.bloodFlowDuration).toBe('3-5 days'); + }); + + it('should set lMPDate to undefined if falsy', () => { + const form = { + value: { + menstrualCycleStatus: null, + cycleLength: null, + bloodFlowDuration: null, + lMPDate: null, + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralMenstrualHistory(form, {}); + expect(result.lMPDate).toBeUndefined(); + }); + + it('should normalize lMPDate when provided', () => { + const form = { + value: { + menstrualCycleStatus: null, + cycleLength: null, + bloodFlowDuration: null, + lMPDate: '2025-06-15', + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralMenstrualHistory(form, {}); + expect(result.lMPDate).toBeDefined(); + expect(result.lMPDate).toContain('2025-06-15'); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralOtherVaccines + // ------------------------------------------------------------------- + describe('postGeneralOtherVaccines', () => { + it('should process vaccine names and output childOptionalVaccineList', () => { + const form = { + value: { + otherVaccines: [ + { vaccineName: { vaccineID: 10, vaccineName: 'MMR' } }, + ], + }, + }; + const result = service.postGeneralOtherVaccines(form, {}); + expect(result.childOptionalVaccineList[0].vaccineID).toBe(10); + expect(result.childOptionalVaccineList[0].vaccineName).toBe('MMR'); + expect(result.otherVaccines).toBeUndefined(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralPersonalHistory + // ------------------------------------------------------------------- + describe('postGeneralPersonalHistory', () => { + it('should process tobacco, alcohol and allergy lists', () => { + const form = { + value: { + tobaccoList: [ + { + tobaccoUseType: { + personalHabitTypeID: 1, + habitValue: 'Cigarettes', + }, + }, + ], + alcoholList: [ + { + typeOfAlcohol: { + personalHabitTypeID: 2, + habitValue: 'Beer', + }, + avgAlcoholConsumption: { habitValue: 'Moderate' }, + }, + ], + allergicList: [ + { + allergyType: { allergyType: 'Drug' }, + typeOfAllergicReactions: [{ allergicReactionTypeID: 99 }], + }, + ], + riskySexualPracticesStatus: null, + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralPersonalHistory(form, {}); + expect(result.tobaccoList[0].tobaccoUseTypeID).toBe(1); + expect(result.tobaccoList[0].tobaccoUseType).toBe('Cigarettes'); + expect(result.alcoholList[0].alcoholTypeID).toBe(2); + expect(result.alcoholList[0].typeOfAlcohol).toBe('Beer'); + expect(result.alcoholList[0].avgAlcoholConsumption).toBe('Moderate'); + expect(result.allergicList[0].allergyType).toBe('Drug'); + expect( + result.allergicList[0].typeOfAllergicReactions[0].allergicReactionTypeID + ).toBe('99'); + }); + + it('should convert riskySexualPracticesStatus to number if truthy', () => { + const form = { + value: { + tobaccoList: [], + alcoholList: [], + allergicList: [], + riskySexualPracticesStatus: '1', + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralPersonalHistory(form, {}); + expect(result.riskySexualPracticesStatus).toBe(1); + }); + + it('should set riskySexualPracticesStatus to null if falsy', () => { + const form = { + value: { + tobaccoList: [], + alcoholList: [], + allergicList: [], + riskySexualPracticesStatus: null, + }, + getRawValue() { + return this.value; + }, + }; + const result = service.postGeneralPersonalHistory(form, {}); + expect(result.riskySexualPracticesStatus).toBeNull(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralPerinatalHistory + // ------------------------------------------------------------------- + describe('postGeneralPerinatalHistory', () => { + it('should process placeOfDelivery, typeOfDelivery, complicationAtBirth', () => { + const form = { + value: { + placeOfDelivery: { + deliveryPlaceID: 1, + deliveryPlace: 'Hospital', + }, + typeOfDelivery: { + deliveryTypeID: 2, + deliveryType: 'Normal', + }, + complicationAtBirth: { + birthComplicationID: 3, + name: 'None', + }, + }, + }; + const result = service.postGeneralPerinatalHistory(form, {}); + expect(result.deliveryPlaceID).toBe(1); + expect(result.placeOfDelivery).toBe('Hospital'); + expect(result.deliveryTypeID).toBe(2); + expect(result.typeOfDelivery).toBe('Normal'); + expect(result.complicationAtBirthID).toBe(3); + expect(result.complicationAtBirth).toBe('None'); + }); + + it('should leave null sub-objects alone', () => { + const form = { + value: { + placeOfDelivery: null, + typeOfDelivery: null, + complicationAtBirth: null, + }, + }; + const result = service.postGeneralPerinatalHistory(form, {}); + expect(result.placeOfDelivery).toBeNull(); + }); + }); + + // ------------------------------------------------------------------- + // postGeneralPastObstetricHistory + // ------------------------------------------------------------------- + describe('postGeneralPastObstetricHistory', () => { + it('should process obstetric history list items', () => { + const form = { + value: { + pastObstericHistoryList: [ + { + durationType: { pregDurationID: 1, durationType: 'Term' }, + deliveryType: { deliveryTypeID: 2, deliveryType: 'Normal' }, + deliveryPlace: { + deliveryPlaceID: 3, + deliveryPlace: 'Hospital', + }, + pregOutcome: { pregOutcomeID: 4, pregOutcome: 'Live Birth' }, + newBornComplication: { + complicationID: 5, + complicationValue: 'None', + }, + }, + ], + }, + }; + const result = service.postGeneralPastObstetricHistory(form, {}); + const item = result.femaleObstetricHistoryList[0]; + expect(item.pregDurationID).toBe(1); + expect(item.durationType).toBe('Term'); + expect(item.deliveryTypeID).toBe(2); + expect(item.deliveryType).toBe('Normal'); + expect(item.deliveryPlaceID).toBe(3); + expect(item.deliveryPlace).toBe('Hospital'); + expect(item.pregOutcomeID).toBe(4); + expect(item.pregOutcome).toBe('Live Birth'); + expect(item.newBornComplicationID).toBe(5); + expect(item.newBornComplication).toBe('None'); + // pastObstericHistoryList should be set to undefined + expect(result.pastObstericHistoryList).toBeUndefined(); + }); + }); + + // ------------------------------------------------------------------- + // postANCForm + // ------------------------------------------------------------------- + describe('postANCForm', () => { + it('should return object with ancObstetricDetails and ancImmunization', () => { + const mockPatientANCForm = { + controls: { + patientANCDetailsForm: { value: { gravida: 2 } }, + obstetricFormulaForm: { + value: { + gravida_G: 2, + termDeliveries_T: 1, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 1, + bloodGroup: 'O+', + }, + }, + patientANCImmunizationForm: { + value: { + dateReceivedForTT_1: null, + dateReceivedForTT_2: null, + dateReceivedForTT_3: null, + }, + }, + }, + }; + const result = service.postANCForm(mockPatientANCForm, 'visit1'); + expect(result.ancObstetricDetails).toBeDefined(); + expect(result.ancImmunization).toBeDefined(); + expect(result.ancObstetricDetails.gravida_G).toBe(2); + expect(result.ancImmunization.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postANCDetailForm + // ------------------------------------------------------------------- + describe('postANCDetailForm', () => { + it('should merge ANC details with obstetric formula and session data', () => { + const mockForm = { + controls: { + patientANCDetailsForm: { + value: { lmpDate: null, expDelDt: null, primiGravida: true }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 3, + termDeliveries_T: 2, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 2, + bloodGroup: 'A+', + }, + }, + }, + }; + const result = service.postANCDetailForm(mockForm, 'visit1'); + expect(result.gravida_G).toBe(3); + expect(result.bloodGroup).toBe('A+'); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe('visit1'); + }); + + it('should normalize lmpDate and expDelDt when provided', () => { + const mockForm = { + controls: { + patientANCDetailsForm: { + value: { + lmpDate: '2025-01-15', + expDelDt: '2025-10-22', + }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 1, + termDeliveries_T: 0, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 0, + bloodGroup: 'B+', + }, + }, + }, + }; + const result = service.postANCDetailForm(mockForm, 'visit1'); + expect(result.lmpDate).toBeDefined(); + expect(result.lmpDate).toContain('2025-01-15'); + expect(result.expDelDt).toBeDefined(); + expect(result.expDelDt).toContain('2025-10-22'); + }); + }); + + // ------------------------------------------------------------------- + // postANCImmunizationForm + // ------------------------------------------------------------------- + describe('postANCImmunizationForm', () => { + it('should normalize TT dates and merge with session data', () => { + const immunizationForm = { + dateReceivedForTT_1: '2025-03-01', + dateReceivedForTT_2: '2025-04-01', + dateReceivedForTT_3: null, + }; + const result = service.postANCImmunizationForm( + immunizationForm, + 'visit1' + ); + expect(result.dateReceivedForTT_1).toContain('2025-03-01'); + expect(result.dateReceivedForTT_2).toContain('2025-04-01'); + expect(result.dateReceivedForTT_3).toBeNull(); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe('visit1'); + }); + }); + + // ------------------------------------------------------------------- + // postPNCDetailForm + // ------------------------------------------------------------------- + describe('postPNCDetailForm', () => { + it('should process delivery details and merge with session data', () => { + const mockPNCForm = { + value: { + deliveryPlace: { + deliveryPlaceID: 1, + deliveryPlace: 'Hospital', + }, + deliveryType: { deliveryTypeID: 2, deliveryType: 'Normal' }, + deliveryComplication: { + complicationID: 3, + deliveryComplicationType: 'None', + }, + pregOutcome: { pregOutcomeID: 4, pregOutcome: 'Live Birth' }, + postNatalComplication: { + complicationID: 5, + complicationValue: 'None', + }, + gestationName: { gestationID: 6, name: 'Full Term' }, + newBornHealthStatus: { + newBornHealthStatusID: 7, + newBornHealthStatus: 'Healthy', + }, + dDate: '2025-06-15', + }, + }; + const result = service.postPNCDetailForm(mockPNCForm, 'visit1'); + expect(result.deliveryPlaceID).toBe(1); + expect(result.deliveryPlace).toBe('Hospital'); + expect(result.deliveryTypeID).toBe(2); + expect(result.deliveryType).toBe('Normal'); + expect(result.deliveryComplicationID).toBe(3); + expect(result.deliveryComplication).toBe('None'); + expect(result.pregOutcomeID).toBe(4); + expect(result.pregOutcome).toBe('Live Birth'); + expect(result.postNatalComplicationID).toBe(5); + expect(result.postNatalComplication).toBe('None'); + expect(result.gestationID).toBe(6); + expect(result.gestationName).toBe('Full Term'); + expect(result.newBornHealthStatusID).toBe(7); + expect(result.newBornHealthStatus).toBe('Healthy'); + expect(result.dateOfDelivery).toBeDefined(); + expect(result.beneficiaryRegID).toBe('9876'); + expect(result.benVisitID).toBe('visit1'); + }); + + it('should handle null sub-objects', () => { + const mockPNCForm = { + value: { + deliveryPlace: null, + deliveryType: null, + deliveryComplication: null, + pregOutcome: null, + postNatalComplication: null, + gestationName: null, + newBornHealthStatus: null, + dDate: null, + }, + }; + const result = service.postPNCDetailForm(mockPNCForm, null); + expect(result.deliveryPlace).toBeNull(); + expect(result.beneficiaryRegID).toBe('9876'); + }); + }); + + // ------------------------------------------------------------------- + // postCancerHistoryDetails + // ------------------------------------------------------------------- + describe('postCancerHistoryDetails', () => { + it('should return empty object when no sub-forms are dirty', () => { + const historyForm = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { dirty: false }, + cancerPatientPerosnalHistoryForm: { dirty: false }, + cancerPatientObstetricHistoryForm: { dirty: false }, + }, + value: { + cancerPatientFamilyMedicalHistoryForm: { diseases: [] }, + cancerPatientPerosnalHistoryForm: {}, + cancerPatientObstetricHistoryForm: {}, + }, + }; + const result = service.postCancerHistoryDetails(historyForm, {}); + expect(Object.keys(result).length).toBe(0); + }); + + it('should include familyHistory when family form is dirty', () => { + const historyForm = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { dirty: true }, + cancerPatientPerosnalHistoryForm: { dirty: false }, + cancerPatientObstetricHistoryForm: { dirty: false }, + }, + value: { + cancerPatientFamilyMedicalHistoryForm: { + diseases: [ + { + cancerDiseaseType: { + cancerDiseaseType: 'Breast Cancer', + }, + otherDiseaseType: null, + }, + ], + }, + cancerPatientPerosnalHistoryForm: {}, + cancerPatientObstetricHistoryForm: {}, + }, + }; + const result: any = service.postCancerHistoryDetails(historyForm, { + beneficiaryRegID: '9876', + }); + expect(result.familyHistory).toBeDefined(); + expect(result.familyHistory.diseases[0].cancerDiseaseType).toBe( + 'Breast Cancer' + ); + }); + + it('should replace "Any other Cancer" with otherDiseaseType', () => { + const historyForm = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { dirty: true }, + cancerPatientPerosnalHistoryForm: { dirty: false }, + cancerPatientObstetricHistoryForm: { dirty: false }, + }, + value: { + cancerPatientFamilyMedicalHistoryForm: { + diseases: [ + { + cancerDiseaseType: { + cancerDiseaseType: 'Any other Cancer', + }, + otherDiseaseType: 'Rare Cancer XYZ', + }, + ], + }, + }, + }; + const result: any = service.postCancerHistoryDetails(historyForm, {}); + expect(result.familyHistory.diseases[0].cancerDiseaseType).toBe( + 'Rare Cancer XYZ' + ); + }); + + it('should include personalHistory when personal form is dirty', () => { + const historyForm = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { dirty: false }, + cancerPatientPerosnalHistoryForm: { dirty: true }, + cancerPatientObstetricHistoryForm: { dirty: false }, + }, + value: { + cancerPatientFamilyMedicalHistoryForm: { diseases: [] }, + cancerPatientPerosnalHistoryForm: { dietType: 'Vegetarian' }, + cancerPatientObstetricHistoryForm: {}, + }, + }; + const result: any = service.postCancerHistoryDetails(historyForm, {}); + expect(result.personalHistory).toBeDefined(); + expect(result.personalHistory.dietType).toBe('Vegetarian'); + }); + + it('should include pastObstetricHistory when obstetric form is dirty', () => { + const historyForm = { + controls: { + cancerPatientFamilyMedicalHistoryForm: { dirty: false }, + cancerPatientPerosnalHistoryForm: { dirty: false }, + cancerPatientObstetricHistoryForm: { dirty: true }, + }, + value: { + cancerPatientFamilyMedicalHistoryForm: { diseases: [] }, + cancerPatientPerosnalHistoryForm: {}, + cancerPatientObstetricHistoryForm: { noOfPregnancies: 2 }, + }, + }; + const result: any = service.postCancerHistoryDetails(historyForm, {}); + expect(result.pastObstetricHistory).toBeDefined(); + expect(result.pastObstetricHistory.noOfPregnancies).toBe(2); + }); + }); + + // ------------------------------------------------------------------- + // postCancerExaminationDetails + // ------------------------------------------------------------------- + describe('postCancerExaminationDetails', () => { + it('should return imageCoordinates always', () => { + const examForm = { + controls: { + signsForm: { dirty: false, controls: {}, value: {} }, + oralExaminationForm: { dirty: false, value: {} }, + breastExaminationForm: { dirty: false, value: {} }, + abdominalExaminationForm: { dirty: false, value: {} }, + gynecologicalExaminationForm: { dirty: false, value: {} }, + }, + }; + const result: any = service.postCancerExaminationDetails(examForm, {}, [ + { x: 1, y: 2 }, + ]); + expect(result.imageCoordinates).toEqual([{ x: 1, y: 2 }]); + }); + + it('should include oralDetails when oral form is dirty', () => { + const examForm = { + controls: { + signsForm: { dirty: false, controls: {}, value: {} }, + oralExaminationForm: { + dirty: true, + value: { + preMalignantLesionTypeList: null, + otherLesionType: null, + image: null, + mouthOpening: 'Normal', + }, + }, + breastExaminationForm: { dirty: false, value: {} }, + abdominalExaminationForm: { dirty: false, value: {} }, + gynecologicalExaminationForm: { dirty: false, value: {} }, + }, + }; + const result: any = service.postCancerExaminationDetails( + examForm, + {}, + [] + ); + expect(result.oralDetails).toBeDefined(); + expect(result.oralDetails.mouthOpening).toBe('Normal'); + expect(result.oralDetails.otherLesionType).toBeUndefined(); + expect(result.oralDetails.image).toBeUndefined(); + }); + + it('should include breastDetails when breast form is dirty', () => { + const examForm = { + controls: { + signsForm: { dirty: false, controls: {}, value: {} }, + oralExaminationForm: { dirty: false, value: {} }, + breastExaminationForm: { + dirty: true, + value: { lumpSize: 'Small', image: 'img1' }, + }, + abdominalExaminationForm: { dirty: false, value: {} }, + gynecologicalExaminationForm: { dirty: false, value: {} }, + }, + }; + const result: any = service.postCancerExaminationDetails( + examForm, + {}, + [] + ); + expect(result.breastDetails).toBeDefined(); + expect(result.breastDetails.lumpSize).toBe('Small'); + expect(result.breastDetails.image).toBeUndefined(); + }); + + it('should include abdominalDetails when abdominal form is dirty', () => { + const examForm = { + controls: { + signsForm: { dirty: false, controls: {}, value: {} }, + oralExaminationForm: { dirty: false, value: {} }, + breastExaminationForm: { dirty: false, value: {} }, + abdominalExaminationForm: { + dirty: true, + value: { liver: 'Enlarged', image: 'img2' }, + }, + gynecologicalExaminationForm: { dirty: false, value: {} }, + }, + }; + const result: any = service.postCancerExaminationDetails( + examForm, + {}, + [] + ); + expect(result.abdominalDetails).toBeDefined(); + expect(result.abdominalDetails.liver).toBe('Enlarged'); + expect(result.abdominalDetails.image).toBeUndefined(); + }); + + it('should include gynecologicalDetails when gynecological form is dirty', () => { + const examForm = { + controls: { + signsForm: { dirty: false, controls: {}, value: {} }, + oralExaminationForm: { dirty: false, value: {} }, + breastExaminationForm: { dirty: false, value: {} }, + abdominalExaminationForm: { dirty: false, value: {} }, + gynecologicalExaminationForm: { + dirty: true, + value: { cervix: 'Normal', image: 'img3' }, + }, + }, + }; + const result: any = service.postCancerExaminationDetails( + examForm, + {}, + [] + ); + expect(result.gynecologicalDetails).toBeDefined(); + expect(result.gynecologicalDetails.cervix).toBe('Normal'); + expect(result.gynecologicalDetails.image).toBeUndefined(); + }); + + it('should include signsDetails with lymphNodes filtering when signs form is dirty', () => { + const mockLymphNodeControl1 = new FormControl({ lymphNodeName: 'Neck' }); + mockLymphNodeControl1.markAsDirty(); + const mockLymphNodeControl2 = new FormControl({ + lymphNodeName: 'Axilla', + }); + // mockLymphNodeControl2 stays pristine + + const lymphNodesArray = new FormArray([ + mockLymphNodeControl1, + mockLymphNodeControl2, + ]); + const signsFormGroup = new FormGroup({ + lymphNodes: lymphNodesArray, + observation: new FormControl('Swelling'), + }); + signsFormGroup.markAsDirty(); + + const examForm = { + controls: { + signsForm: signsFormGroup, + oralExaminationForm: { dirty: false, value: {} }, + breastExaminationForm: { dirty: false, value: {} }, + abdominalExaminationForm: { dirty: false, value: {} }, + gynecologicalExaminationForm: { dirty: false, value: {} }, + }, + }; + const result: any = service.postCancerExaminationDetails( + examForm, + { beneficiaryRegID: '9876' }, + [] + ); + expect(result.signsDetails).toBeDefined(); + expect(result.signsDetails.cancerSignAndSymptoms).toBeDefined(); + expect(result.signsDetails.cancerLymphNodeDetails.length).toBe(1); + expect(result.signsDetails.cancerLymphNodeDetails[0].lymphNodeName).toBe( + 'Neck' + ); + }); + }); + + // ------------------------------------------------------------------- + // Nurse visit form methods (POST to HTTP) + // ------------------------------------------------------------------- + describe('nurse visit form POST methods', () => { + function buildFullMedicalForm() { + return { + controls: { + patientVisitForm: buildMockVisitForm(), + patientVitalsForm: buildMockVitalsForm(), + patientHistoryForm: buildMockHistoryControls(), + patientExaminationForm: { value: buildMockExaminationFormValue() }, + patientANCForm: { + controls: { + patientANCDetailsForm: { + value: { lmpDate: null, expDelDt: null }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 1, + termDeliveries_T: 0, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 0, + bloodGroup: 'O+', + }, + }, + patientANCImmunizationForm: { + value: { + dateReceivedForTT_1: null, + dateReceivedForTT_2: null, + dateReceivedForTT_3: null, + }, + }, + }, + }, + patientPNCForm: { + value: { + deliveryPlace: null, + deliveryType: null, + deliveryComplication: null, + pregOutcome: null, + postNatalComplication: null, + gestationName: null, + newBornHealthStatus: null, + dDate: null, + }, + }, + NCDScreeningForm: { value: {} }, + idrsScreeningForm: { value: {} }, + }, + }; + } + + it('postNurseANCVisitForm should POST to saveNurseANCDetails', () => { + const medicalForm = buildFullMedicalForm(); + service + .postNurseANCVisitForm(medicalForm, 'visit1', 'ANC', 25) + .subscribe(); + const req = httpMock.expectOne(environment.saveNurseANCDetails); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.ancDetails).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + expect(req.request.body.examinationDetails).toBeDefined(); + expect(req.request.body.benFlowID).toBe('222'); + expect(req.request.body.beneficiaryID).toBe('333'); + expect(req.request.body.sessionID).toBe('sess1'); + expect(req.request.body.parkingPlaceID).toBe(20); + expect(req.request.body.vanID).toBe(10); + expect(req.request.body.serviceID).toBe('5'); + expect(req.request.body.createdBy).toBe('testUser'); + req.flush({}); + }); + + it('postNurseGeneralOPDVisitForm should POST to saveNurseGeneralOPDDetails', () => { + const medicalForm = buildFullMedicalForm(); + service + .postNurseGeneralOPDVisitForm(medicalForm, 'General OPD', { + ageVal: 30, + }) + .subscribe(); + const req = httpMock.expectOne(environment.saveNurseGeneralOPDDetails); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + expect(req.request.body.examinationDetails).toBeDefined(); + expect(req.request.body.vanID).toBe(10); + req.flush({}); + }); + + it('postNurseNCDCareVisitForm should POST to saveNurseNCDCareDetails', () => { + const medicalForm = buildFullMedicalForm(); + service + .postNurseNCDCareVisitForm(medicalForm, 'NCD care', { ageVal: 50 }) + .subscribe(); + const req = httpMock.expectOne(environment.saveNurseNCDCareDetails); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + // NCD care does not include examinationDetails + expect(req.request.body.examinationDetails).toBeUndefined(); + req.flush({}); + }); + + it('postNurseCovidCareVisitForm should POST to saveNurseCovidCareDetails', () => { + const medicalForm = buildFullMedicalForm(); + service + .postNurseCovidCareVisitForm(medicalForm, 'COVID-19 Screening', { + ageVal: 40, + }) + .subscribe(); + const req = httpMock.expectOne(environment.saveNurseCovidCareDetails); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + req.flush({}); + }); + + it('postNursePNCVisitForm should POST to savePNCNurseDetailsUrl', () => { + const medicalForm = buildFullMedicalForm(); + service + .postNursePNCVisitForm(medicalForm, 'PNC', { ageVal: 28 }) + .subscribe(); + const req = httpMock.expectOne(environment.savePNCNurseDetailsUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.pNCDeatils).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + expect(req.request.body.examinationDetails).toBeDefined(); + req.flush({}); + }); + + it('postNurseGeneralQCVisitForm should POST to saveNurseGeneralQuickConsult', () => { + const medicalForm = { + controls: { + patientVisitForm: { + controls: { + patientVisitDetailsForm: { + value: { visitReason: 'Quick Consult' }, + }, + patientFileUploadDetailsForm: { value: {} }, + }, + }, + patientVitalsForm: { value: { height: 170 } }, + }, + }; + service.postNurseGeneralQCVisitForm(medicalForm).subscribe(); + const req = httpMock.expectOne(environment.saveNurseGeneralQuickConsult); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.vitalsDetails).toBeDefined(); + expect(req.request.body.benFlowID).toBe('222'); + expect(req.request.body.parkingPlaceID).toBe(20); + expect(req.request.body.vanID).toBe(10); + req.flush({}); + }); + + it('postNCDScreeningForm should POST to postNCDScreeningDetails', () => { + const medicalForm = buildFullMedicalForm(); + service.postNCDScreeningForm(medicalForm, 'NCD screening').subscribe(); + const req = httpMock.expectOne(environment.postNCDScreeningDetails); + expect(req.request.method).toBe('POST'); + expect(req.request.body.visitDetails).toBeDefined(); + expect(req.request.body.vitalDetails).toBeDefined(); + expect(req.request.body.historyDetails).toBeDefined(); + expect(req.request.body.idrsDetails).toBeDefined(); + expect(req.request.body.benFlowID).toBe('222'); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // postNurseCancerVisitForm + // ------------------------------------------------------------------- + describe('postNurseCancerVisitForm', () => { + it('should POST to saveNurseCancerScreeningDetails with session metadata', () => { + const medicalForm = { + controls: { + patientVisitForm: { + controls: { + patientVisitDetailsForm: { dirty: true, value: {} }, + patientFileUploadDetailsForm: { value: {} }, + }, + }, + patientVitalsForm: { dirty: true, value: { weight: 60 } }, + patientExaminationForm: { + dirty: false, + controls: {}, + value: {}, + }, + patientHistoryForm: { + dirty: false, + controls: {}, + value: {}, + }, + }, + }; + service.postNurseCancerVisitForm(medicalForm, [], true).subscribe(); + const req = httpMock.expectOne( + environment.saveNurseCancerScreeningDetails + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body.sendToDoctorWorklist).toBeTrue(); + expect(req.request.body.benFlowID).toBe('222'); + expect(req.request.body.beneficiaryID).toBe('333'); + expect(req.request.body.sessionID).toBe('sess1'); + expect(req.request.body.vanID).toBe(10); + expect(req.request.body.parkingPlaceID).toBe(20); + expect(req.request.body.serviceID).toBe('5'); + expect(req.request.body.createdBy).toBe('testUser'); + req.flush({}); + }); + + it('should include visitDetails only when visit form is dirty', () => { + const medicalForm = { + controls: { + patientVisitForm: { + controls: { + patientVisitDetailsForm: { dirty: false, value: {} }, + patientFileUploadDetailsForm: { value: {} }, + }, + }, + patientVitalsForm: { dirty: false, value: {} }, + patientExaminationForm: { dirty: false, controls: {}, value: {} }, + patientHistoryForm: { dirty: false, controls: {}, value: {} }, + }, + }; + service.postNurseCancerVisitForm(medicalForm, [], false).subscribe(); + const req = httpMock.expectOne( + environment.saveNurseCancerScreeningDetails + ); + expect(req.request.body.visitDetails).toBeUndefined(); + expect(req.request.body.vitalsDetails).toBeUndefined(); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // saveBenCovidVaccinationDetails + // ------------------------------------------------------------------- + describe('saveBenCovidVaccinationDetails', () => { + it('should POST with covidVSID when provided', () => { + const form = { + value: { + covidVSID: 123, + vaccineStatus: 'Vaccinated', + vaccineTypes: 'Covaxin', + doseTaken: 'Dose1', + }, + }; + service.saveBenCovidVaccinationDetails(form).subscribe(); + const req = httpMock.expectOne( + environment.saveCovidVaccinationDetailsUrl + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body.covidVSID).toBe(123); + expect(req.request.body.modifiedBy).toBe('testUser'); + expect(req.request.body.vanID).toBe(10); + expect(req.request.body.parkingPlaceID).toBe(20); + req.flush({}); + }); + + it('should POST with covidVSID as null when not provided', () => { + const form = { + value: { + covidVSID: null, + vaccineStatus: 'Not Vaccinated', + vaccineTypes: null, + doseTaken: null, + }, + }; + service.saveBenCovidVaccinationDetails(form).subscribe(); + const req = httpMock.expectOne( + environment.saveCovidVaccinationDetailsUrl + ); + expect(req.request.body.covidVSID).toBeNull(); + expect(req.request.body.modifiedBy).toBeUndefined(); + req.flush({}); + }); + + it('should POST with covidVSID as null when undefined', () => { + const form = { + value: { + covidVSID: undefined, + vaccineStatus: 'Not Vaccinated', + vaccineTypes: null, + doseTaken: null, + }, + }; + service.saveBenCovidVaccinationDetails(form).subscribe(); + const req = httpMock.expectOne( + environment.saveCovidVaccinationDetailsUrl + ); + expect(req.request.body.covidVSID).toBeNull(); + req.flush({}); + }); + }); + + // ------------------------------------------------------------------- + // normalizeToUTCMidnight (tested indirectly via public methods) + // ------------------------------------------------------------------- + describe('normalizeToUTCMidnight (indirect)', () => { + it('should normalize a date to UTC midnight ISO string via postANCDetailForm', () => { + const mockForm = { + controls: { + patientANCDetailsForm: { + value: { lmpDate: '2025-10-07', expDelDt: null }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 1, + termDeliveries_T: 0, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 0, + bloodGroup: 'O+', + }, + }, + }, + }; + const result = service.postANCDetailForm(mockForm, 'visit1'); + expect(result.lmpDate).toMatch(/2025-10-07T00:00:00\.000Z/); + }); + + it('should return null for null date via postANCDetailForm', () => { + const mockForm = { + controls: { + patientANCDetailsForm: { + value: { lmpDate: null, expDelDt: null }, + }, + obstetricFormulaForm: { + value: { + gravida_G: 1, + termDeliveries_T: 0, + pretermDeliveries_P: 0, + abortions_A: 0, + stillBirth: 0, + livebirths_L: 0, + bloodGroup: 'O+', + }, + }, + }, + }; + const result = service.postANCDetailForm(mockForm, 'visit1'); + // lmpDate stays null because normalizeToUTCMidnight returns null for falsy input + // but the if-guard prevents calling normalizeToUTCMidnight when lmpDate is null + expect(result.lmpDate).toBeNull(); + }); + }); + + // ------------------------------------------------------------------- + // fileData property + // ------------------------------------------------------------------- + describe('fileData', () => { + it('should allow setting and getting fileData', () => { + expect(service.fileData).toBeUndefined(); + service.fileData = [1, 2, 3]; + expect(service.fileData).toEqual([1, 2, 3]); + }); + }); + + // ------------------------------------------------------------------- + // rbsTestResultFromDoctorFetch + // ------------------------------------------------------------------- + describe('rbsTestResultFromDoctorFetch', () => { + it('should allow setting and reading rbsTestResultFromDoctorFetch', () => { + expect(service.rbsTestResultFromDoctorFetch).toBeUndefined(); + service.rbsTestResultFromDoctorFetch = 'someResult'; + expect(service.rbsTestResultFromDoctorFetch).toBe('someResult'); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/services/test-in-vitals.service.spec.ts b/src/app/app-modules/nurse-doctor/shared/services/test-in-vitals.service.spec.ts index 71f11abe..f885d455 100644 --- a/src/app/app-modules/nurse-doctor/shared/services/test-in-vitals.service.spec.ts +++ b/src/app/app-modules/nurse-doctor/shared/services/test-in-vitals.service.spec.ts @@ -20,21 +20,76 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; import { TestInVitalsService } from './test-in-vitals.service'; describe('TestInVitalsService', () => { + let service: TestInVitalsService; + beforeEach(() => { TestBed.configureTestingModule({ providers: [TestInVitalsService], }); + service = TestBed.inject(TestInVitalsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('initial state', () => { + it('should have null vitalRBSTest', () => { + expect(service.vitalRBSTest).toBeNull(); + }); + + it('should have null vitalRBSTestUpdate', () => { + expect(service.vitalRBSTestUpdate).toBeNull(); + }); + }); + + describe('setVitalsRBSValueInReports', () => { + it('should set the RBS value and emit through observable', done => { + service.vitalRBSTestResult$.subscribe(val => { + if (val === 150) { + expect(service.vitalRBSTest).toBe(150); + done(); + } + }); + service.setVitalsRBSValueInReports(150); + }); + }); + + describe('clearVitalsRBSValueInReports', () => { + it('should reset RBS value to 0', () => { + service.setVitalsRBSValueInReports(150); + service.clearVitalsRBSValueInReports(); + expect(service.vitalRBSTest).toBe(0); + let val: any; + service.vitalRBSTestResult$.subscribe(v => (val = v)); + expect(val).toBe(0); + }); }); - it('should be created', inject( - [TestInVitalsService], - (service: TestInVitalsService) => { - expect(service).toBeTruthy(); - } - )); + describe('setVitalsRBSValueInReportsInUpdate', () => { + it('should set the update RBS value and emit', done => { + service.vitalRBSTestResultInUpdate$.subscribe(val => { + if (val === 200) { + expect(service.vitalRBSTestUpdate).toBe(200); + done(); + } + }); + service.setVitalsRBSValueInReportsInUpdate(200); + }); + }); + + describe('clearVitalsRBSValueInReportsInUpdate', () => { + it('should reset update RBS value to 0', () => { + service.setVitalsRBSValueInReportsInUpdate(200); + service.clearVitalsRBSValueInReportsInUpdate(); + expect(service.vitalRBSTestUpdate).toBe(0); + let val: any; + service.vitalRBSTestResultInUpdate$.subscribe(v => (val = v)); + expect(val).toBe(0); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.spec.ts new file mode 100644 index 00000000..855afd7b --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/autocomplete-scroller.directive.spec.ts @@ -0,0 +1,217 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { Component, ViewChild } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { AutocompleteScrollerDirective } from './autocomplete-scroller.directive'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { Subject } from 'rxjs'; +import { NgZone } from '@angular/core'; + +describe('AutocompleteScrollerDirective', () => { + let directive: AutocompleteScrollerDirective; + let mockAutocomplete: any; + let openedSubject: Subject; + let closedSubject: Subject; + let mockNgZone: jasmine.SpyObj; + + beforeEach(() => { + openedSubject = new Subject(); + closedSubject = new Subject(); + + mockAutocomplete = { + opened: openedSubject.asObservable(), + closed: closedSubject.asObservable(), + panel: null, + id: 'test-autocomplete', + }; + + mockNgZone = jasmine.createSpyObj('NgZone', ['run', 'runOutsideAngular']); + mockNgZone.run.and.callFake((fn: any) => fn()); + mockNgZone.runOutsideAngular.and.callFake((fn: any) => fn()); + + directive = new AutocompleteScrollerDirective( + mockAutocomplete as MatAutocomplete, + mockNgZone as any + ); + }); + + afterEach(() => { + directive.ngOnDestroy(); + }); + + it('should create an instance', () => { + expect(directive).toBeTruthy(); + }); + + it('should have default threshold of 0.6', () => { + expect(directive.threshold).toBe(0.6); + }); + + it('should allow custom threshold', () => { + directive.threshold = 0.8; + expect(directive.threshold).toBe(0.8); + }); + + it('should clean up subscriptions on destroy', () => { + directive.ngAfterViewInit(); + directive.ngOnDestroy(); + openedSubject.next(); + expect(mockNgZone.runOutsideAngular).not.toHaveBeenCalled(); + }); + + describe('with a panel element', () => { + let rafQueue: FrameRequestCallback[]; + const flushRaf = () => { + while (rafQueue.length) rafQueue.shift()!(0); + }; + const makePanel = ( + scrollHeight: number, + clientHeight: number, + scrollTop: number + ) => { + const el: any = { + scrollHeight, + clientHeight, + scrollTop, + listener: null as any, + addEventListener: jasmine + .createSpy('addEventListener') + .and.callFake((_: string, fn: any) => (el.listener = fn)), + removeEventListener: jasmine.createSpy('removeEventListener'), + }; + return el; + }; + + beforeEach(() => { + rafQueue = []; + spyOn(window, 'requestAnimationFrame').and.callFake((cb: any) => { + rafQueue.push(cb); + return rafQueue.length; + }); + }); + + it('emits panelReady and attaches a passive scroll listener when panel opens', () => { + const panel = makePanel(200, 100, 0); + mockAutocomplete.panel = { nativeElement: panel }; + const ready = jasmine.createSpy('ready'); + directive.panelReady.subscribe(ready); + directive.ngAfterViewInit(); + openedSubject.next(); + flushRaf(); + expect(ready).toHaveBeenCalledWith(panel); + expect(panel.addEventListener).toHaveBeenCalledWith( + 'scroll', + jasmine.any(Function), + { passive: true } + ); + }); + + it('emits nearEnd only past the threshold and when panel overflows', () => { + const panel = makePanel(200, 100, 0); + mockAutocomplete.panel = { nativeElement: panel }; + const near = jasmine.createSpy('near'); + directive.nearEnd.subscribe(near); + directive.ngAfterViewInit(); + openedSubject.next(); + flushRaf(); + + panel.listener(); // ratio 0.5 < 0.6 + expect(near).not.toHaveBeenCalled(); + + panel.scrollTop = 50; // ratio 0.75 + panel.listener(); + expect(near).toHaveBeenCalledTimes(1); + expect(mockNgZone.run).toHaveBeenCalled(); + + panel.scrollHeight = 100; // no overflow + panel.listener(); + expect(near).toHaveBeenCalledTimes(1); + }); + + it('retries until the panel exists, then gives up after max tries', () => { + const ready = jasmine.createSpy('ready'); + directive.panelReady.subscribe(ready); + mockAutocomplete.id = undefined; + directive.ngAfterViewInit(); + openedSubject.next(); + // first frame + 10 retries + flushRaf(); + expect((window.requestAnimationFrame as jasmine.Spy).calls.count()).toBe( + 11 + ); + expect(ready).not.toHaveBeenCalled(); + }); + + it('falls back to finding the panel by id', () => { + const el = document.createElement('div'); + el.id = 'test-autocomplete'; + document.body.appendChild(el); + try { + const ready = jasmine.createSpy('ready'); + directive.panelReady.subscribe(ready); + directive.ngAfterViewInit(); + openedSubject.next(); + flushRaf(); + expect(ready).toHaveBeenCalledWith(el); + } finally { + directive.ngOnDestroy(); + document.body.removeChild(el); + } + }); + + it('removes the scroll listener when the panel closes', () => { + const panel = makePanel(200, 100, 0); + mockAutocomplete.panel = { nativeElement: panel }; + directive.ngAfterViewInit(); + openedSubject.next(); + flushRaf(); + const fn = panel.listener; + closedSubject.next(); + expect(panel.removeEventListener).toHaveBeenCalledWith('scroll', fn); + // a second close has nothing to remove + closedSubject.next(); + expect(panel.removeEventListener).toHaveBeenCalledTimes(1); + }); + + it('removes the scroll listener on destroy', () => { + const panel = makePanel(200, 100, 0); + mockAutocomplete.panel = { nativeElement: panel }; + directive.ngAfterViewInit(); + openedSubject.next(); + flushRaf(); + directive.ngOnDestroy(); + expect(panel.removeEventListener).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/cancer-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/cancer-utility.spec.ts new file mode 100644 index 00000000..f9575e37 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/cancer-utility.spec.ts @@ -0,0 +1,64 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { FormBuilder, FormGroup } from '@angular/forms'; +import { createSessionStorageMock } from 'src/testing/test-utils'; +import { CancerUtils } from './cancer-utility'; + +describe('CancerUtils', () => { + let utils: CancerUtils; + let session: ReturnType; + + beforeEach(() => { + session = createSessionStorageMock({ + serviceLineDetails: JSON.stringify({ vanID: 5, parkingPlaceID: 6 }), + }); + utils = new CancerUtils(new FormBuilder(), session as any); + }); + + it('builds a FormGroup from every create*/init* factory, stamping van details', () => { + const names = Object.getOwnPropertyNames(CancerUtils.prototype).filter(n => + /^(create|init)/.test(n) + ); + expect(names.length).toBeGreaterThan(10); + names.forEach(n => { + const f = (utils as any)[n](); + expect(f instanceof FormGroup) + .withContext(n) + .toBeTrue(); + if (f.contains('vanID')) { + expect(f.get('vanID').value).withContext(n).toBe(5); + expect(f.get('parkingPlaceID').value).withContext(n).toBe(6); + } + }); + }); + + it('ngOnInIt reads service line details from session', () => { + utils.ngOnInIt(); + expect(session.getItem).toHaveBeenCalledWith('serviceLineDetails'); + }); + + it('family medical history starts with one disease row', () => { + const f = utils.createCancerPatientFamilyMedicalHistoryForm(); + expect((f.get('diseases') as any).length).toBe(1); + expect((f.get('diseases') as any).at(0).get('vanID').value).toBe(5); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/general-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/general-utility.spec.ts new file mode 100644 index 00000000..662578b5 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/general-utility.spec.ts @@ -0,0 +1,199 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { createSessionStorageMock } from 'src/testing/test-utils'; +import { GeneralUtils } from './general-utility'; + +describe('GeneralUtils', () => { + let utils: GeneralUtils; + let session: ReturnType; + + beforeEach(() => { + session = createSessionStorageMock({ + serviceLineDetails: JSON.stringify({ vanID: 11, parkingPlaceID: 22 }), + }); + utils = new GeneralUtils(new FormBuilder(), session as any); + }); + + const factoryNames = () => + Object.getOwnPropertyNames(GeneralUtils.prototype).filter( + n => /^(create|init)/.test(n) && n !== 'initMedicineWithData' + ); + + it('builds a FormGroup from every create*/init* factory', () => { + const names = factoryNames(); + expect(names.length).toBeGreaterThan(50); + names.forEach(name => { + const result = (utils as any)[name](1); + expect(result instanceof FormGroup) + .withContext(name) + .toBeTrue(); + }); + }); + + it('stamps vanID/parkingPlaceID from session on forms that carry them', () => { + let stamped = 0; + for (const name of factoryNames()) { + const form: FormGroup = (utils as any)[name](1); + if (!form.contains('vanID')) continue; + stamped++; + expect(form.get('vanID')?.value).withContext(name).toBe(11); + expect(form.get('parkingPlaceID')?.value).withContext(name).toBe(22); + } + expect(stamped).toBeGreaterThan(20); + expect(session.getItem).toHaveBeenCalledWith('serviceLineDetails'); + }); + + it('composes the general history form from its sub-forms', () => { + const form = utils.createGeneralHistoryForm(false); + [ + 'pastHistory', + 'comorbidityHistory', + 'medicationHistory', + 'personalHistory', + 'familyHistory', + 'menstrualHistory', + 'perinatalHistory', + 'pastObstericHistory', + 'immunizationHistory', + 'otherVaccines', + 'feedingHistory', + 'developmentHistory', + ].forEach(k => + expect(form.get(k) instanceof FormGroup) + .withContext(k) + .toBeTrue() + ); + }); + + it('composes the NCD screening history form', () => { + const form = utils.createNCDScreeningHistoryForm(); + ['physicalActivityHistory', 'familyHistory', 'personalHistory'].forEach(k => + expect(form.get(k) instanceof FormGroup) + .withContext(k) + .toBeTrue() + ); + expect( + (form.get('familyHistory.familyDiseaseList') as FormArray).length + ).toBe(0); + }); + + it('composes every case record with findings, diagnosis, investigation and prescription', () => { + ([ + utils.createGeneralCaseRecord(), + utils.createANCCaseRecord(), + utils.createNCDCareCaseRecord(), + utils.createCovidCareCaseRecord(), + utils.createNCDScreeningCaseRecord(), + utils.createPNCCaseRecord(), + ]).forEach((f, i) => + [ + 'generalFindingsForm', + 'generalDiagnosisForm', + 'generalDoctorInvestigationForm', + 'drugPrescriptionForm', + ].forEach(k => + expect(f.get(k) instanceof FormGroup) + .withContext(`${i}:${k}`) + .toBeTrue() + ) + ); + }); + + it('initialises past obstetric history with the given pregnancy order and empty arrays', () => { + expect(utils.initPastObstericHistory(3).get('pregOrder')?.value).toBe(3); + const past = utils.createPastObstericHistoryForm(); + expect((past.get('pastObstericHistoryList') as FormArray).length).toBe(0); + expect((past.get('complicationPregList') as FormArray).length).toBe(0); + }); + + it('marks provisional and confirmatory diagnosis concept/term as required', () => { + const prov: FormGroup = utils.initProvisionalDiagnosisList(); + expect(prov.valid).toBeFalse(); + prov.patchValue({ + conceptID: 'c1', + term: 't1', + provisionalDiagnosis: 'p', + }); + expect(prov.valid).toBeTrue(); + const conf = utils.initConfirmatoryDiagnosisList(); + expect(conf.get('conceptID')?.valid).toBeFalse(); + conf.patchValue({ conceptID: 'c', term: 't' }); + expect(conf.valid).toBeTrue(); + }); + + it('starts general diagnosis with one provisional diagnosis row', () => { + const f = utils.createGeneralDiagnosisForm(); + expect((f.get('provisionalDiagnosisList') as FormArray).length).toBe(1); + }); + + it('starts drug prescription form with an empty prescribedDrugs array', () => { + const f = utils.createDrugPrescriptionForm(); + expect((f.get('prescribedDrugs') as FormArray).length).toBe(0); + expect(f.get('vanID')?.value).toBe(11); + }); + + describe('initMedicineWithData', () => { + const base = { + drugID: 5, + drugName: 'Paracetamol', + drugStrength: '500', + formName: 'Tablet', + dose: '1', + qtyPrescribed: 10, + frequency: 'BD', + duration: 5, + route: 'Oral', + unit: 'Days', + instructions: 'after food', + sctCode: 'S1', + sctTerm: 'T1', + isEDL: true, + }; + + it('concatenates strength and unit when drugUnit is present', () => { + const f = utils.initMedicineWithData( + { ...base, drugUnit: 'mg', createdBy: 'nurse' }, + 7 as any + ); + expect(f.value).toEqual( + jasmine.objectContaining({ + id: 7, + drugID: 5, + drugStrength: '500mg', + durationView: '5 Days', + createdBy: 'nurse', + vanID: 11, + parkingPlaceID: 22, + isEDL: true, + }) + ); + }); + + it('keeps plain strength and a null createdBy when absent', () => { + const f = utils.initMedicineWithData({ ...base }); + expect(f.get('drugStrength')?.value).toBe('500'); + expect(f.get('createdBy')?.value).toBeNull(); + expect(f.get('id')?.value).toBeNull(); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/ncd-screening-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/ncd-screening-utility.spec.ts new file mode 100644 index 00000000..fb20b816 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/ncd-screening-utility.spec.ts @@ -0,0 +1,55 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { FormBuilder, FormGroup } from '@angular/forms'; +import { createSessionStorageMock } from 'src/testing/test-utils'; +import { NCDScreeningUtils } from './ncd-screening-utility'; + +describe('NCDScreeningUtils', () => { + let utils: NCDScreeningUtils; + let session: ReturnType; + + beforeEach(() => { + session = createSessionStorageMock({ + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + }); + utils = new NCDScreeningUtils(new FormBuilder(), session as any); + }); + + it('creates the NCD screening form with van details and null readings', () => { + const f: FormGroup = utils.createNCDScreeningForm(); + expect(f.get('vanID')?.value).toBe(1); + expect(f.get('parkingPlaceID')?.value).toBe(2); + expect(f.get('systolicBP_1stReading')?.value).toBeNull(); + expect(f.get('averageDiastolicBP_Reading')?.value).toBeNull(); + expect(f.contains('labTestOrders')).toBeTrue(); + expect(f.contains('isBloodGlucosePrescribed')).toBeTrue(); + expect(session.getItem).toHaveBeenCalledWith('serviceLineDetails'); + }); + + it('creates the IDRS form with deleted=false and van details', () => { + const f = utils.createIDRSForm(); + expect(f.get('deleted')?.value).toBeFalse(); + expect(f.get('idrsScore')?.value).toBeNull(); + expect(f.get('vanID')?.value).toBe(1); + expect(f.get('parkingPlaceID')?.value).toBe(2); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/quick-consult-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/quick-consult-utility.spec.ts new file mode 100644 index 00000000..c4b97cd3 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/quick-consult-utility.spec.ts @@ -0,0 +1,109 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { createSessionStorageMock } from 'src/testing/test-utils'; +import { QuickConsultUtils } from './quick-consult-utility'; + +describe('QuickConsultUtils', () => { + let utils: QuickConsultUtils; + + beforeEach(() => { + const session = createSessionStorageMock({ + serviceLineDetails: JSON.stringify({ vanID: 9, parkingPlaceID: 8 }), + }); + utils = new QuickConsultUtils(new FormBuilder(), session as any); + }); + + it('initMedicine builds an empty medicine row with disabled qih', () => { + const f = utils.initMedicine(); + expect(f.get('drug')?.value).toBeNull(); + expect(f.get('qih')?.disabled).toBeTrue(); + expect(f.get('vanID')?.value).toBe(9); + expect(f.get('parkingPlaceID')?.value).toBe(8); + }); + + it('initChiefComplaint carries van details', () => { + expect(utils.initChiefComplaint().value).toEqual({ + chiefComplaint: null, + conceptID: null, + description: null, + vanID: 9, + parkingPlaceID: 8, + }); + }); + + it('createQuickConsultForm requires observation, diagnosisProvided and a provisional diagnosis', () => { + const f: FormGroup = utils.createQuickConsultForm(); + expect(f.valid).toBeFalse(); + expect(f.get('clinicalObservation')?.hasError('required')).toBeTrue(); + expect(f.get('diagnosisProvided')?.hasError('required')).toBeTrue(); + expect(f.get('beneficiaryName')?.disabled).toBeTrue(); + expect(f.get('rbsTestResult')?.enabled).toBeTrue(); + expect((f.get('chiefComplaintList') as FormArray).length).toBe(1); + const diag = (f.get('provisionalDiagnosisList') as FormArray).at(0); + expect(diag.get('term')?.hasError('required')).toBeTrue(); + expect((f.get('prescription.prescribedDrugs') as FormArray).length).toBe(0); + expect(f.get('vanID')?.value).toBe(9); + + f.patchValue({ clinicalObservation: 'ok', diagnosisProvided: 'd' }); + diag.patchValue({ conceptID: 'c', term: 't', provisionalDiagnosis: 'p' }); + expect(f.valid).toBeTrue(); + }); + + it('initMedicineWithData concatenates strength and unit', () => { + const f = utils.initMedicineWithData( + { + drugID: 1, + drugName: 'X', + drugStrength: '10', + drugUnit: 'mg', + duration: 3, + unit: 'Days', + createdBy: 'doc', + isEDL: false, + }, + 4 as any + ); + expect(f.value).toEqual( + jasmine.objectContaining({ + id: 4, + drugStrength: '10mg', + durationView: '3 Days', + createdBy: 'doc', + vanID: 9, + parkingPlaceID: 8, + isEDL: false, + }) + ); + }); + + it('initMedicineWithData uses plain strength when unit missing', () => { + const f = utils.initMedicineWithData({ + drugStrength: '250', + duration: 1, + unit: 'Week', + }); + expect(f.get('drugStrength')?.value).toBe('250'); + expect(f.get('createdBy')?.value).toBeNull(); + expect(f.get('durationView')?.value).toBe('1 Week'); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/validation-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/validation-utility.spec.ts new file mode 100644 index 00000000..415cc3fb --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/validation-utility.spec.ts @@ -0,0 +1,65 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { ValidationUtils } from './validation-utility'; + +describe('ValidationUtils', () => { + const v = new ValidationUtils(); + + describe('getAgeValue', () => { + it('converts each unit to days', () => { + expect(v.getAgeValue('2 years')).toBe(730); + expect(v.getAgeValue('3 Months')).toBe(90); + expect(v.getAgeValue('2 weeks')).toBe(14); + expect(v.getAgeValue(' 5 days ')).toBe(5); + }); + it('returns 0 for empty or unknown units', () => { + expect(v.getAgeValue('')).toBe(0); + expect(v.getAgeValue(null)).toBe(0); + expect(v.getAgeValue('4 decades')).toBe(0); + }); + }); + + describe('validateDuration', () => { + const age = '1 years - 2 months - 3 days'; // 365 + 60 + 3 = 428 days + + it('accepts durations up to the beneficiary age for every unit', () => { + expect(v.validateDuration(24 * 428, 'Hours', age)).toBeTrue(); + expect(v.validateDuration(428, 'Days', age)).toBeTrue(); + expect(v.validateDuration(61, 'Weeks', age)).toBeTrue(); + expect(v.validateDuration(14, 'Months', age)).toBeTrue(); + expect(v.validateDuration(1, 'Years', age)).toBeTrue(); + }); + + it('rejects durations longer than the beneficiary age', () => { + expect(v.validateDuration(429, 'Days', age)).toBeFalse(); + expect(v.validateDuration(62, 'Weeks', age)).toBeFalse(); + expect(v.validateDuration(15, 'Months', age)).toBeFalse(); + expect(v.validateDuration(2, 'Years', age)).toBeFalse(); + expect(v.validateDuration(24 * 429, 'Hours', age)).toBeFalse(); + }); + + it('treats unknown units (including singular forms) as zero days', () => { + expect(v.validateDuration(999, 'Fortnights', '1 days')).toBeTrue(); + expect(v.validateDuration(999, 'Year', '1 days')).toBeTrue(); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/shared/utility/visit-detail-utility.spec.ts b/src/app/app-modules/nurse-doctor/shared/utility/visit-detail-utility.spec.ts new file mode 100644 index 00000000..0487bdc4 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/shared/utility/visit-detail-utility.spec.ts @@ -0,0 +1,133 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { createSessionStorageMock } from 'src/testing/test-utils'; +import { VisitDetailUtils } from './visit-detail-utility'; + +describe('VisitDetailUtils', () => { + let utils: VisitDetailUtils; + + beforeEach(() => { + const session = createSessionStorageMock({ + serviceLineDetails: JSON.stringify({ vanID: 3, parkingPlaceID: 4 }), + }); + utils = new VisitDetailUtils(new FormBuilder(), session as any); + }); + + it('composes the patient visit form with all sub-forms', () => { + const f = utils.createPatientVisitForm(); + [ + 'patientVisitDetailsForm', + 'patientChiefComplaintsForm', + 'patientAdherenceForm', + 'patientInvestigationsForm', + 'patientCovidForm', + 'patientFileUploadDetailsForm', + 'patientDiseaseForm', + 'tmcConfirmationForm', + 'covidVaccineStatusForm', + ].forEach(k => + expect(f.get(k) instanceof FormGroup) + .withContext(k) + .toBeTrue() + ); + expect(f.get('patientVisitDetailsForm.vanID')?.value).toBe(3); + expect(f.get('patientCovidForm.parkingPlaceID')?.value).toBe(4); + }); + + it('leaves editable controls enabled by default', () => { + const f = utils.createPatientVisitForm(); + expect(f.get('patientVisitDetailsForm.visitReason')?.enabled).toBeTrue(); + expect(f.get('patientVisitDetailsForm.visitCode')?.disabled).toBeTrue(); + expect(f.get('patientAdherenceForm.toDrugs')?.enabled).toBeTrue(); + expect(f.get('patientInvestigationsForm.laboratoryList')?.value).toEqual( + [] + ); + }); + + it('disables editable controls when the disable flag is set', () => { + const f = utils.createPatientVisitForm(true); + expect(f.get('patientVisitDetailsForm.visitReason')?.disabled).toBeTrue(); + expect(f.get('patientAdherenceForm.progress')?.disabled).toBeTrue(); + expect( + f.get('patientInvestigationsForm.laboratoryList')?.disabled + ).toBeTrue(); + expect(f.get('patientCovidForm.symptom')?.disabled).toBeTrue(); + const complaint = ( + f.get('patientChiefComplaintsForm.complaints') as FormArray + ).at(0); + expect(complaint.get('chiefComplaint')?.disabled).toBeTrue(); + expect(complaint.get('duration')?.disabled).toBeTrue(); + }); + + it('covid form requires symptom and contact status when enabled', () => { + const f = utils.createPatientCovidForm(); + expect(f.get('symptom')?.hasError('required')).toBeTrue(); + expect(f.get('contactStatus')?.hasError('required')).toBeTrue(); + f.patchValue({ symptom: 'y', contactStatus: 'n' }); + expect(f.valid).toBeTrue(); + }); + + it('builds simple sub-forms with default args', () => { + expect( + utils.createPatientFileUploadDetailsForm().get('fileIDs')?.value + ).toBeNull(); + expect( + (utils.createPatientDiseaseForm().get('diseaseFormsArray') as FormArray) + .length + ).toBe(0); + expect(utils.createPatientTmcConfirmationForm().value).toEqual({ + tmcConfirmed: null, + refrredToAdditionalServiceList: null, + isDiabetic: null, + isHypertensionConfirmed: null, + }); + expect( + utils.createPatientChiefComplaintsForm().get('chiefComplaint')?.enabled + ).toBeTrue(); + }); + + it('maps disease data (note: reads data.disease, not diseaseName)', () => { + const f = utils.createPatientDiseaseArrayForm({ + disease: 'Diabetes', + flag: true, + selected: false, + }); + expect(f.value).toEqual({ + diseaseName: 'Diabetes', + flag: true, + selected: false, + }); + }); + + it('defaults disease data to nulls when none is given', () => { + const f = utils.createPatientDiseaseArrayForm(null); + expect(f.get('diseaseName')?.value).toBeNull(); + expect(f.get('flag')?.value).toBeNull(); + expect(f.get('selected')?.value).toBeNull(); + }); + + it('creates the covid vaccine status form with null defaults', () => { + const f = utils.createCovidVaccineStatusForm(false); + expect(Object.values(f.value).every(v => v === null)).toBeTrue(); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/sms-notification/sms-notification.component.spec.ts b/src/app/app-modules/nurse-doctor/sms-notification/sms-notification.component.spec.ts index 2e9261c4..a44c7a0d 100644 --- a/src/app/app-modules/nurse-doctor/sms-notification/sms-notification.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/sms-notification/sms-notification.component.spec.ts @@ -1,15 +1,124 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { BehaviorSubject, of, throwError } from 'rxjs'; +import { + MatDialogRef, + MAT_DIALOG_DATA, + MatDialog, +} from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { SmsNotificationComponent } from './sms-notification.component'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SmsTemplateService } from '../smsTemplate/sms-template.service'; +import { ConfirmationService } from '../../core/services'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('SmsNotificationComponent', () => { let component: SmsNotificationComponent; let fixture: ComponentFixture; + let mockHttpService: jasmine.SpyObj; + let mockSmsService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockDialogRef: jasmine.SpyObj>; + let mockDialog: jasmine.SpyObj; + let mockSnackBar: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + + const mockLanguageObject = { sms: 'SMS', send: 'Send' }; + + const mockDialogData = { + prescribedDrugs: [ + { + beneficiaryRegID: '123', + prescribedDrugID: '456', + drug: 'Paracetamol', + strength: '500mg', + frequency: 'Twice daily', + noOfDays: 5, + remarks: 'After food', + diagnosisProvided: 'Fever', + prescriptionID: 'P001', + }, + { + beneficiaryRegID: '123', + prescribedDrugID: '789', + drug: 'Amoxicillin', + strength: '250mg', + frequency: 'Thrice daily', + noOfDays: 7, + remarks: 'Before food', + diagnosisProvided: 'Infection', + prescriptionID: 'P002', + }, + ], + }; + + beforeEach(waitForAsync(() => { + mockHttpService = jasmine.createSpyObj( + 'HttpServiceService', + ['fetchLanguageSet'], + { + currentLangugae$: new BehaviorSubject( + mockLanguageObject + ).asObservable(), + } + ); + mockSmsService = jasmine.createSpyObj('SmsTemplateService', [ + 'getSMStypes', + 'getSMStemplates', + 'sendSMS', + ]); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + mockDialogRef = jasmine.createSpyObj('MatDialogRef', ['close']); + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); - beforeEach(() => { TestBed.configureTestingModule({ declarations: [SmsNotificationComponent], - }); + providers: [ + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SmsTemplateService, useValue: mockSmsService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: MatDialog, useValue: mockDialog }, + { provide: MatSnackBar, useValue: mockSnackBar }, + { provide: MAT_DIALOG_DATA, useValue: mockDialogData }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { fixture = TestBed.createComponent(SmsNotificationComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -18,4 +127,243 @@ describe('SmsNotificationComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should create MatTableDataSource from prescribedDrugs', () => { + expect(component.dataSource).toBeDefined(); + expect(component.dataSource.data).toEqual(mockDialogData.prescribedDrugs); + }); + + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngOnInit(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('ngDoCheck', () => { + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngDoCheck(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + }); + + describe('assignSelectedLanguage', () => { + it('should set currentLanguageSet', () => { + component.assignSelectedLanguage(); + expect(component.currentLanguageSet).toEqual(mockLanguageObject); + }); + }); + + describe('mobileNum', () => { + it('should set validNumber to true when value length is 10', () => { + component.mobileNum('9876543210'); + expect(component.validNumber).toBeTrue(); + }); + + it('should set validNumber to false when value length is less than 10', () => { + component.mobileNum('12345'); + expect(component.validNumber).toBeFalse(); + }); + + it('should set validNumber to false when value length is more than 10', () => { + component.mobileNum('12345678901'); + expect(component.validNumber).toBeFalse(); + }); + + it('should set validNumber to false for empty string', () => { + component.mobileNum(''); + expect(component.validNumber).toBeFalse(); + }); + }); + + describe('sendSMS', () => { + it('should not proceed when currentServiceID is undefined', () => { + mockSessionStorage.getItem.and.returnValue(undefined); + component.sendSMS(); + expect(mockSmsService.getSMStypes).not.toHaveBeenCalled(); + }); + + it('should chain SMS service calls and close dialog on success', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + const store: Record = { + currentServiceID: 'svc1', + providerServiceMapID: 'psm1', + phnum: '9876543210', + userName: 'testUser', + providerServiceID: 'ps1', + }; + return store[key]; + }); + + mockSmsService.getSMStypes.and.returnValue( + of({ + data: [ + { smsType: 'MMUPrescription SMS', smsTypeID: 'type1' }, + { smsType: 'Other SMS', smsTypeID: 'type2' }, + ], + }) + ); + + mockSmsService.getSMStemplates.and.returnValue( + of({ + data: [ + { smsTemplateID: 'tpl1', deleted: false }, + { smsTemplateID: 'tpl2', deleted: true }, + ], + }) + ); + + mockSmsService.sendSMS.and.returnValue(of({ statusCode: 200 })); + + component.sendSMS(); + + expect(mockSmsService.getSMStypes).toHaveBeenCalledWith('svc1'); + expect(mockSmsService.getSMStemplates).toHaveBeenCalledWith( + 'psm1', + 'type1' + ); + expect(mockSmsService.sendSMS).toHaveBeenCalled(); + expect(mockSnackBar.open).toHaveBeenCalledWith( + 'SMS sent successfully', + 'Close', + jasmine.objectContaining({ duration: 3000 }) + ); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Data saved successfully', + 'success' + ); + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + + it('should use mobileNumber when phnum is Not Available', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + const store: Record = { + currentServiceID: 'svc1', + providerServiceMapID: 'psm1', + phnum: 'Not Available', + userName: 'testUser', + providerServiceID: 'ps1', + }; + return store[key]; + }); + + mockSmsService.getSMStypes.and.returnValue( + of({ + data: [{ smsType: 'MMUPrescription SMS', smsTypeID: 'type1' }], + }) + ); + mockSmsService.getSMStemplates.and.returnValue( + of({ data: [{ smsTemplateID: 'tpl1', deleted: false }] }) + ); + mockSmsService.sendSMS.and.returnValue(of({ statusCode: 200 })); + + component.mobileNumber = '1234567890'; + component.sendSMS(); + + const sendSMSArg = mockSmsService.sendSMS.calls.mostRecent().args[0]; + expect(sendSMSArg[0].alternateNo).toBe('1234567890'); + }); + + it('should show error snackbar on SMS send failure', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + const store: Record = { + currentServiceID: 'svc1', + providerServiceMapID: 'psm1', + phnum: '9876543210', + userName: 'testUser', + providerServiceID: 'ps1', + }; + return store[key]; + }); + + mockSmsService.getSMStypes.and.returnValue( + of({ + data: [{ smsType: 'MMUPrescription SMS', smsTypeID: 'type1' }], + }) + ); + mockSmsService.getSMStemplates.and.returnValue( + of({ data: [{ smsTemplateID: 'tpl1', deleted: false }] }) + ); + mockSmsService.sendSMS.and.returnValue( + throwError(() => new Error('Network error')) + ); + + component.sendSMS(); + + expect(mockSnackBar.open).toHaveBeenCalledWith( + 'SMS not sent', + 'Close', + jasmine.objectContaining({ duration: 3000 }) + ); + }); + }); + + describe('allowOnlyNumbers', () => { + it('should prevent non-numeric keypress', () => { + const event = { + which: 65, + keyCode: 65, + preventDefault: jasmine.createSpy('preventDefault'), + } as unknown as KeyboardEvent; + + component.allowOnlyNumbers(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should allow numeric keypress (digit 0 = charCode 48)', () => { + const event = { + which: 48, + keyCode: 48, + preventDefault: jasmine.createSpy('preventDefault'), + } as unknown as KeyboardEvent; + + component.allowOnlyNumbers(event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should allow numeric keypress (digit 9 = charCode 57)', () => { + const event = { + which: 57, + keyCode: 57, + preventDefault: jasmine.createSpy('preventDefault'), + } as unknown as KeyboardEvent; + + component.allowOnlyNumbers(event); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should use keyCode when which is 0', () => { + const event = { + which: 0, + keyCode: 65, + preventDefault: jasmine.createSpy('preventDefault'), + } as unknown as KeyboardEvent; + + component.allowOnlyNumbers(event); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('onClose', () => { + it('should close the dialog', () => { + component.onClose(); + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + }); + + describe('displayedColumns', () => { + it('should have the expected columns', () => { + expect(component.displayedColumns).toEqual([ + 'prescriptionID', + 'diagnosisProvided', + 'drug', + 'strength', + 'frequency', + 'noOfDays', + 'remarks', + ]); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/smsTemplate/sms-template.service.spec.ts b/src/app/app-modules/nurse-doctor/smsTemplate/sms-template.service.spec.ts index ca9bad6f..9a02d3a6 100644 --- a/src/app/app-modules/nurse-doctor/smsTemplate/sms-template.service.spec.ts +++ b/src/app/app-modules/nurse-doctor/smsTemplate/sms-template.service.spec.ts @@ -21,18 +21,92 @@ */ import { TestBed } from '@angular/core/testing'; - +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { SmsTemplateService } from './sms-template.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { environment } from 'src/environments/environment'; +import { BehaviorSubject } from 'rxjs'; describe('SmsTemplateService', () => { let service: SmsTemplateService; + let httpMock: HttpTestingController; beforeEach(() => { - TestBed.configureTestingModule({}); + const mockHttpServiceService = { + currentLangugae$: new BehaviorSubject({ language: 'English' }), + }; + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + SmsTemplateService, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + ], + }); + service = TestBed.inject(SmsTemplateService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + describe('getSMStemplates', () => { + it('should POST with providerServiceMapID and smsTypeID', () => { + service.getSMStemplates('PSM001', 5).subscribe(); + const req = httpMock.expectOne(environment.getSMStemplates_url); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + providerServiceMapID: 'PSM001', + smsTemplateTypeID: 5, + }); + req.flush({ statusCode: 200 }); + }); + + it('should POST with undefined smsTemplateTypeID when not provided', () => { + service.getSMStemplates('PSM001').subscribe(); + const req = httpMock.expectOne(environment.getSMStemplates_url); + expect(req.request.body.smsTemplateTypeID).toBeUndefined(); + req.flush({ statusCode: 200 }); + }); + }); + + describe('getSMSTemplates', () => { + it('should POST with providerServiceMapID only', () => { + service.getSMSTemplates('PSM002').subscribe(); + const req = httpMock.expectOne(environment.getSMStemplates_url); + expect(req.request.body).toEqual({ + providerServiceMapID: 'PSM002', + }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('getSMStypes', () => { + it('should POST with serviceID', () => { + service.getSMStypes(2).subscribe(); + const req = httpMock.expectOne(environment.getSMStypes_url); + expect(req.request.body).toEqual({ serviceID: 2 }); + req.flush({ statusCode: 200 }); + }); + }); + + describe('sendSMS', () => { + it('should POST the SMS object', () => { + const smsObj = { phoneNumber: '1234567890', message: 'Test' }; + service.sendSMS(smsObj).subscribe(); + const req = httpMock.expectOne(environment.sendSMS_url); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(smsObj); + req.flush({ statusCode: 200 }); + }); + }); }); diff --git a/src/app/app-modules/nurse-doctor/tm-visit-details/tm-visit-details.component.spec.ts b/src/app/app-modules/nurse-doctor/tm-visit-details/tm-visit-details.component.spec.ts index ed3008fd..fb13e31b 100644 --- a/src/app/app-modules/nurse-doctor/tm-visit-details/tm-visit-details.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/tm-visit-details/tm-visit-details.component.spec.ts @@ -20,27 +20,175 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DoctorService, MasterdataService } from '../shared/services'; import { TmVisitDetailsComponent } from './tm-visit-details.component'; describe('TmVisitDetailsComponent', () => { let component: TmVisitDetailsComponent; let fixture: ComponentFixture; + let doctor: any; + let session: any; + let router: any; + + const makeForm = () => + new FormGroup({ + patientVisitDetailsForm: new FormGroup({ + visitCategory: new FormControl(null), + visitReason: new FormControl(null), + subVisitCategory: new FormControl(null), + }), + tmcConfirmationForm: new FormGroup({ + tmcConfirmed: new FormControl(false), + }), + }); + + const benData = { VisitCategory: 'NCD screening', VisitReason: 'Screening' }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + async function setup( + seed: Record, + preg: any = { statusCode: 200, data: null } + ) { + doctor = autoSpy(DoctorService); + doctor.getPregVisitComplaintDetails.and.returnValue(of(preg)); + router = { + url: '/nurse-doctor/attendant/nurse', + navigate: jasmine.createSpy('navigate'), + }; + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [TmVisitDetailsComponent], + providers: [ + ...commonTestProviders({ + session: { + visitCategory: 'NCD screening', + visitID: 'v1', + beneficiaryRegID: 'b1', + beneficiaryData: JSON.stringify(benData), + ...seed, + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: MasterdataService, useValue: autoSpy(MasterdataService) }, + { provide: Router, useValue: router }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(TmVisitDetailsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + session = TestBed.inject(SessionStorageService); + component.patientVisitForm = makeForm(); + component.ngOnInit(); + } + + afterEach(() => { + sessionStorage.removeItem('selectTMC'); + sessionStorage.removeItem('specialist_flag'); + sessionStorage.removeItem('beneficiaryData'); + }); + + it('patches visit details from beneficiary data and pregnancy status', async () => { + await setup( + {}, + { + statusCode: 200, + data: { NCDScreeningNurseVisitDetail: { subVisitCategory: 'sub' } }, + } + ); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('NCD screening'); + expect(doctor.getPregVisitComplaintDetails).toHaveBeenCalledWith( + 'b1', + 'v1', + 'NCD screening' + ); + expect(component.patientVisitDetailsForm.value).toEqual({ + visitCategory: 'NCD screening', + visitReason: 'Screening', + subVisitCategory: 'sub', + }); + expect(component.tmcConfirmationForm.value.tmcConfirmed).toBeFalse(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('marks TMC confirmed when selectTMC is set', async () => { + const rm = spyOn(sessionStorage, 'removeItem').and.callThrough(); + await setup({ selectTMC: true }); + expect(component.tmcConfirmationForm.value.tmcConfirmed).toBeTrue(); + expect(rm).toHaveBeenCalledWith('selectTMC'); + }); + + it('ignores null / non-200 pregnancy responses', async () => { + await setup({}, null); + expect(component.patientVisitDetailsForm.value.subVisitCategory).toBeNull(); + doctor.getPregVisitComplaintDetails.and.returnValue( + of({ statusCode: 500 }) + ); + component.getPregnancyStatus(); + expect(component.patientVisitDetailsForm.value.subVisitCategory).toBeNull(); + }); + + it('getVisitDetails skips patch when no beneficiary data', async () => { + await setup({}); + session.store.set('beneficiaryData', 'null'); + component.patientVisitForm = makeForm(); + component.getVisitDetails(); + expect(component.beneficiaryData).toBeNull(); + expect( + component.patientVisitForm.get('patientVisitDetailsForm')?.value + .visitCategory + ).toBeNull(); + }); + + it('ngOnDestroy on print page keeps TMC selection', async () => { + await setup({}); + router.url = '/nurse-doctor/print/MMU/current'; + doctor.prescribedDrugData = 'x'; + component.ngOnDestroy(); + expect(session.setItem).toHaveBeenCalledWith('selectTMC', 'true'); + expect(doctor.prescribedDrugData).toBe('x'); + }); + + it('ngOnDestroy elsewhere clears data and unsubscribes', async () => { + await setup({}); + const rm = spyOn(sessionStorage, 'removeItem').and.callThrough(); + const u = spyOn(component.visitDetailsPregSubscription, 'unsubscribe'); + doctor.prescribedDrugData = 'x'; + component.ngOnDestroy(); + expect(rm).toHaveBeenCalledWith('specialist_flag'); + expect(rm).toHaveBeenCalledWith('beneficiaryData'); + expect(u).toHaveBeenCalled(); + expect(doctor.prescribedDrugData).toBeNull(); + component.visitDetailsPregSubscription = null; + component.ngOnDestroy(); + expect(doctor.prescribedDrugData).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('conditionCheck stores category and resets hideAll', async () => { + await setup({}); + component.hideAll = true; + component.conditionCheck(); + expect(session.setItem).toHaveBeenCalledWith( + 'visiCategoryANC', + 'NCD screening' + ); + expect(component.hideAll).toBeFalse(); + component.hideAll = true; + component.visitCategory = 'ANC'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.hideAll).toBeFalse(); }); }); diff --git a/src/app/app-modules/nurse-doctor/tm-visit-details/tmcconfirmation/tmcconfirmation.component.spec.ts b/src/app/app-modules/nurse-doctor/tm-visit-details/tmcconfirmation/tmcconfirmation.component.spec.ts index ef711ba1..f8278d77 100644 --- a/src/app/app-modules/nurse-doctor/tm-visit-details/tmcconfirmation/tmcconfirmation.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/tm-visit-details/tmcconfirmation/tmcconfirmation.component.spec.ts @@ -20,27 +20,379 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { DataSyncLoginComponent } from 'src/app/app-modules/core/components/data-sync-login/data-sync-login.component'; +import { DoctorService, NurseService } from '../../shared/services'; +import { MasterdataService } from '../../shared/services/masterdata.service'; +import { IdrsscoreService } from '../../shared/services/idrsscore.service'; import { TmcconfirmationComponent } from './tmcconfirmation.component'; describe('TmcconfirmationComponent', () => { let component: TmcconfirmationComponent; let fixture: ComponentFixture; + let doctor: any; + let nurse: any; + let master: any; + let idrs: any; + let confirm: any; + let dialog: any; + let session: any; + let router: Router; + let visitMaster$: BehaviorSubject; + + const centers = [ + { institutionID: 1, institutionName: 'AIIMS' }, + { institutionID: 2, institutionName: 'KEM' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const makeTmcForm = (tmcConfirmed = false) => + new FormGroup({ + tmcConfirmed: new FormControl(tmcConfirmed), + refrredToAdditionalServiceList: new FormControl(null), + isHypertensionConfirmed: new FormControl(null), + isDiabetic: new FormControl(null), + }); + + async function setup( + opts: { + tmcConfirmed?: boolean; + hyper?: boolean; + diab?: boolean; + drugs?: any; + seed?: Record; + category?: string | null; + visitMaster?: any; + } = {} + ) { + doctor = autoSpy(DoctorService, { prescribedDrugData: opts.drugs }); + nurse = autoSpy(NurseService); + visitMaster$ = new BehaviorSubject(opts.visitMaster ?? null); + master = autoSpy(MasterdataService, { + visitDetailMasterData$: visitMaster$.asObservable(), + }); + master.getVisitDetailMasterData.and.returnValue(undefined); + idrs = autoSpy(IdrsscoreService, { + isHypertensionConfirmed: !!opts.hyper, + isDiabeticsConfirmed: !!opts.diab, + }); + idrs.setTMCSubmit.and.returnValue(undefined); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [TmcconfirmationComponent], + providers: [ + ...commonTestProviders({ + session: { + beneficiaryRegID: 'b1', + visitID: 'v1', + providerServiceID: 'p1', + ...(opts.seed || {}), + }, + }), + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: MasterdataService, useValue: master }, + { provide: IdrsscoreService, useValue: idrs }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(TmcconfirmationComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + spyOn(console, 'log'); + component.tmcConfirmationFormsGroup = makeTmcForm(opts.tmcConfirmed); + component.patientVisitDetailsForm = new FormGroup({ + visitCategory: new FormControl( + opts.category === undefined ? 'NCD screening' : opts.category + ), + }); + component.ngOnInit(); + } + + afterEach(() => { + component?.ngOnDestroy(); + sessionStorage.removeItem('tmCaseSheet'); + sessionStorage.removeItem('authorizeToViewTMcasesheet'); + }); + + describe('ngOnInit', () => { + it('defaults when TMC not confirmed', async () => { + await setup(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.visitCategory).toBe('NCD screening'); + expect(master.getVisitDetailMasterData).toHaveBeenCalled(); + expect(component.showRadio).toBeFalse(); + expect(component.disableNoOnSuccessOfYes).toBeFalse(); + expect(component.prescribedDrugDataFromCaseSheet).toBeUndefined(); + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('patches confirmed flags from IDRS service when TMC confirmed', async () => { + await setup({ + tmcConfirmed: true, + hyper: true, + diab: true, + drugs: ['d'], + seed: { disableNoOnSuccessOfYes: 'true' }, + }); + expect(component.showRadio).toBeTrue(); + expect( + component.tmcConfirmationFormsGroup.value.isHypertensionConfirmed + ).toBeTrue(); + expect(component.tmcConfirmationFormsGroup.value.isDiabetic).toBeTrue(); + expect(component.prescribedDrugDataFromCaseSheet).toEqual(['d']); + expect(component.disableNoOnSuccessOfYes).toBeTrue(); + }); + + it('patches false flags when TMC confirmed but nothing confirmed', async () => { + await setup({ tmcConfirmed: true }); + expect( + component.tmcConfirmationFormsGroup.value.isHypertensionConfirmed + ).toBeFalse(); + expect(component.tmcConfirmationFormsGroup.value.isDiabetic).toBeFalse(); + }); + }); + + describe('master data and refer details', () => { + const visitMaster = { + visitCategories: [ + { visitCategory: 'NCD screening', visitCategoryID: 7 }, + { visitCategory: 'ANC', visitCategoryID: 4 }, + ], + }; + + it('loads doctor master data and patches the referred centre', async () => { + await setup(); + master.getDoctorMasterDataForNurse.and.returnValue( + of({ statusCode: 200, data: { higherHealthCare: centers } }) + ); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: { Refer: { referredToInstituteID: 2 } } }) + ); + visitMaster$.next(visitMaster); + expect(session.setItem).toHaveBeenCalledWith( + 'caseSheetVisitCategoryID', + 7 + ); + expect(master.getDoctorMasterDataForNurse).toHaveBeenCalledWith(7, 'p1'); + expect(doctor.getCaseRecordAndReferDetails).toHaveBeenCalledWith( + 'b1', + 'v1', + 'NCD screening' + ); + expect(component.defaultCentre).toBe('KEM'); + expect( + component.tmcConfirmationFormsGroup.value.refrredToAdditionalServiceList + ).toEqual(centers[1]); + }); + + it('does not patch when referred centre is unknown or refer missing', async () => { + await setup(); + component.higherHealthcareCenter = centers; + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: { Refer: { referredToInstituteID: 9 } } }) + ); + component.getReferDetails(); + doctor.getCaseRecordAndReferDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getReferDetails(); + expect(component.defaultCentre).toBeUndefined(); + }); + + it('skips doctor master data on non-200', async () => { + await setup(); + master.getDoctorMasterDataForNurse.and.returnValue( + of({ statusCode: 500, data: null }) + ); + visitMaster$.next(visitMaster); + expect(doctor.getCaseRecordAndReferDetails).not.toHaveBeenCalled(); + }); + + it('does nothing without a category or unknown category', async () => { + await setup({ category: null, visitMaster }); + expect(component.visitCategoryList).toEqual(visitMaster.visitCategories); + expect(master.getDoctorMasterDataForNurse).not.toHaveBeenCalled(); + expect(component.getVisitCategoryID('Unknown')).toBeNull(); + component.visitCategory = 'Unknown'; + component.getDoctorMasterData('Unknown'); + expect(master.getDoctorMasterDataForNurse).not.toHaveBeenCalled(); + expect(component.getVisitCategoryID('ANC')).toBe(4); + }); + }); + + it('checkDiabetes patches isDiabetic on success only', async () => { + await setup(); + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: { isDiabetic: true } }) + ); + component.checkDiabetes(); + expect(nurse.getPreviousVisitData).toHaveBeenCalledWith({ benRegID: 'b1' }); + expect(component.tmcConfirmationFormsGroup.value.isDiabetic).toBeTrue(); + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 500, data: { isDiabetic: false } }) + ); + component.checkDiabetes(); + expect(component.tmcConfirmationFormsGroup.value.isDiabetic).toBeTrue(); + }); + + it('resetAdditionalServiceList toggles radio, validator and TMC submit', async () => { + await setup(); + const ctrl = + component.tmcConfirmationFormsGroup.controls[ + 'refrredToAdditionalServiceList' + ]; + component.resetAdditionalServiceList({ value: false }); + expect(idrs.setTMCSubmit).toHaveBeenCalledWith(false); + expect(component.showRadio).toBeFalse(); + ctrl.updateValueAndValidity(); + expect(ctrl.valid).toBeFalse(); + component.resetAdditionalServiceList({ value: true }); + expect(idrs.setTMCSubmit).toHaveBeenCalledWith(true); + expect(component.showRadio).toBeTrue(); + expect(ctrl.errors).toBeNull(); + }); + + it('higherhealthcarecenter flags selection', async () => { + await setup(); + component.higherhealthcarecenter(null); + expect(component.selectValueService).toBeUndefined(); + component.higherhealthcarecenter({ institutionName: 'A' }); + expect(component.selectValueService).toBeTrue(); + }); + + describe('view TM casesheet', () => { + const ben = { + benFlowID: 11, + VisitCategory: 'NCD screening', + beneficiaryRegID: 22, + benVisitID: 33, + visitCode: 44, + }; + const okRes = (disease: any) => ({ + statusCode: 200, + data: { + nurseData: { idrs: { IDRSDetail: { confirmedDisease: disease } } }, + }, + }); + + beforeEach(async () => + setup({ seed: { beneficiaryData: JSON.stringify(ben) } }) + ); + + it('stores casesheet keys, fetches casesheet and routes on confirm', () => { + nurse.getTMReferredCasesheetData.and.returnValue( + of(okRes(['Hypertension', 'Diabetes'])) + ); + component.viewAndPrintCaseSheet(); + expect(session.setItem).toHaveBeenCalledWith('caseSheetTMFlag', 'true'); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalledWith({ + VisitCategory: 'NCD screening', + benFlowID: 11, + benVisitID: 33, + beneficiaryRegID: 22, + visitCode: 44, + }); + expect(idrs.setTMCSubmit).toHaveBeenCalledWith(false); + expect(sessionStorage.getItem('tmCaseSheet')).toBe('true'); + expect(component.disableNoOnSuccessOfYes).toBe('true'); + expect(idrs.isHypertensionConfirmed).toBeTrue(); + expect(idrs.isDiabeticsConfirmed).toBeTrue(); + expect(component.tmcConfirmationFormsGroup.value.isDiabetic).toBeTrue(); + expect(confirm.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.consulation + ); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/print/MMU/current', + ]); + expect(component.caseSheetData).toBeDefined(); + }); + + it('does not route when confirm declined; handles no confirmed disease', () => { + confirm.confirm.and.returnValue(of(false)); + nurse.getTMReferredCasesheetData.and.returnValue(of(okRes(null))); + component.getTMReferredCasesheetData({}); + expect(router.navigate).not.toHaveBeenCalled(); + expect(idrs.isHypertensionConfirmed).toBeFalse(); + expect(idrs.isDiabeticsConfirmed).toBeFalse(); + expect( + component.tmcConfirmationFormsGroup.value.isHypertensionConfirmed + ).toBeFalse(); + }); + + it('opens login dialog on 5003 and re-authorizes', () => { + nurse.getTMReferredCasesheetData.and.returnValues( + of({ statusCode: 5003 }), + of({ statusCode: 500, errorMessage: 'x' }) + ); + dialog.open.and.returnValue(createDialogRefMock(true)); + sessionStorage.setItem('authorizeToViewTMcasesheet', 'Authorized'); + component.getTMReferredCasesheetData({}); + expect(dialog.open).toHaveBeenCalledWith( + DataSyncLoginComponent, + jasmine.objectContaining({ + disableClose: true, + data: { provideAuthorizationToViewTmCS: true }, + }) + ); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalledTimes(2); + expect(confirm.alert).toHaveBeenCalledWith('x', 'error'); + }); + + it('does not re-authorize when dialog closed without authorization', () => { + nurse.getTMReferredCasesheetData.and.returnValue( + of({ statusCode: 5003 }) + ); + dialog.open.and.returnValue(createDialogRefMock(true)); + component.getTMReferredCasesheetData({}); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalledTimes(1); + }); + + it('alerts and re-enables submit on error', () => { + nurse.getTMReferredCasesheetData.and.returnValue(throwingObs()); + component.getTMReferredCasesheetData({}); + expect(idrs.setTMCSubmit).toHaveBeenCalledWith(true); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.errorInfetchingTMCasesheet, + 'error' + ); + }); + + it('skips fetching when not TM flagged and not specialist', () => { + spyOn(component, 'setCasesheetData'); + component.onceAuthorizeViewTMCS(); + expect(nurse.getTMReferredCasesheetData).not.toHaveBeenCalled(); + session.store.set('specialistFlag', '200'); + nurse.getTMReferredCasesheetData.and.returnValue(of({ statusCode: 500 })); + component.onceAuthorizeViewTMCS(); + expect(nurse.getTMReferredCasesheetData).toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes master data', async () => { + await setup(); + const u = spyOn(component.visitDetailMasterDataSubscription, 'unsubscribe'); + component.ngOnDestroy(); + expect(u).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/adherence/adherence.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/adherence/adherence.component.spec.ts index 78b84b76..f139608a 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/adherence/adherence.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/adherence/adherence.component.spec.ts @@ -20,61 +20,110 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { VisitDetailUtils } from '../../shared/utility'; - + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; import { DoctorService } from '../../shared/services'; -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - import { AdherenceComponent } from './adherence.component'; describe('AdherenceComponent', () => { let component: AdherenceComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let doctor: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [AdherenceComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: DoctorService, useValue: doctor }, + ], schemas: [NO_ERRORS_SCHEMA], - providers: [{ provide: DoctorService, useClass: DoctorServiceStub }], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(AdherenceComponent, ''); fixture = TestBed.createComponent(AdherenceComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + component.patientAdherenceForm = new FormGroup({ + toDrugs: new FormControl(null), + drugReason: new FormControl('old drug'), + toReferral: new FormControl(null), + referralReason: new FormControl('old ref'), + progress: new FormControl(null), + }); + }); + + it('ngOnInit and ngDoCheck set the language', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.adherenceProgressData).toEqual([ + 'Improved', + 'Unchanged', + 'Worsened', + ]); + }); + + it('ngOnChanges in view mode patches adherence details', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ + statusCode: 200, + data: { BenAdherence: { toDrugs: true, drugReason: 'none' } }, + }) + ); + component.mode = 'view'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.toDrugs).toBeTrue(); + expect(component.drugReason).toBe('none'); + }); + + it('does not patch on non-200 response', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 5000, data: { BenAdherence: { toDrugs: true } } }) + ); + component.getAdherenceDetails('b1', 'v1'); + expect(component.toDrugs).toBeNull(); + }); + + it('does not patch when BenAdherence is null', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: { BenAdherence: null } }) + ); + component.getAdherenceDetails('b1', 'v1'); + expect(component.toDrugs).toBeNull(); + }); + + it('ngOnChanges outside view mode does nothing', () => { + component.mode = 'add'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + }); - fb = debugElement.injector.get(FormBuilder); - component.patientAdherenceForm = new VisitDetailUtils( - fb - ).createPatientAdherenceForm(); - fixture.detectChanges(); + it('checkReferralDescription clears reason only when truthy', () => { + component.checkReferralDescription(false); + expect(component.referralReason).toBe('old ref'); + component.checkReferralDescription(true); + expect(component.referralReason).toBeNull(); + expect(component.toReferral).toBeNull(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('checkDrugsDescription clears reason only when truthy', () => { + component.checkDrugsDescription(false); + expect(component.drugReason).toBe('old drug'); + component.checkDrugsDescription(true); + expect(component.drugReason).toBeNull(); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/chief-complaints/chief-complaints.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/chief-complaints/chief-complaints.component.spec.ts index 517318d1..d8199406 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/chief-complaints/chief-complaints.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/chief-complaints/chief-complaints.component.spec.ts @@ -20,198 +20,411 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { VisitDetailUtils } from '../../shared/utility'; - + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../shared/services'; import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; - -import { MasterdataServiceStub } from '../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../core/mocks/beneficiary-details-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; +import { VisitDetailUtils } from '../../shared/utility/visit-detail-utility'; import { ChiefComplaintsComponent } from './chief-complaints.component'; describe('ChiefComplaintsComponent', () => { let component: ChiefComplaintsComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let master: any; + let doctor: any; + let nurse: any; + let confirm: any; + let tracking: any; + let nurseMasterData$: BehaviorSubject; + let beneficiary$: BehaviorSubject; + let utils: VisitDetailUtils; + const originalOffline = environment.isMMUOfflineSync; + + const fever = { chiefComplaint: 'Fever', chiefComplaintID: 1 }; + const cough = { chiefComplaint: 'Cough', chiefComplaintID: 2 }; + const headache = { chiefComplaint: 'Headache', chiefComplaintID: 3 }; + const masterList = () => [cough, fever, headache]; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + nurseMasterData$ = new BehaviorSubject(null); + beneficiary$ = new BehaviorSubject({ age: '2 years' }); + master = autoSpy(MasterdataService, { nurseMasterData$ }); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ChiefComplaintsComponent], - schemas: [NO_ERRORS_SCHEMA], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ + session: { + visitID: 'v1', + beneficiaryRegID: 'b1', + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: beneficiary$ }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(ChiefComplaintsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.patientChiefComplaintsForm = new VisitDetailUtils( - fb - ).createANCPatientChiefComplaintArrayForm(); - window.console.log = () => {}; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should call getBeneficiaryDetails on Initialisation', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); - expect(component.beneficiary).toEqual(data.femaleBeneficiary); - } - )); - - it('should clear ChiefComplaintsForm when total complaints is one and remove button is clicked', () => { - const complaint = component.patientChiefComplaintsForm.controls[ - 'complaints' - ] as FormArray; - complaint.controls[0].patchValue({ - chiefComplaint: { - chiefComplaintID: 1, - chiefComplaint: 'Abdominal Bloating', - }, - beneficiaryRegID: 7416, - benVisitID: 863, - providerServiceMapID: 1320, - duration: 12, - unitOfDuration: 'Hours', - description: 'hghgjhgjh', - createdBy: null, - }); - fixture.detectChanges(); - component.removeCheifComplaint(0, complaint.controls[0] as FormGroup); - expect(complaint.value.length).toBe(1); - expect( - component.patientChiefComplaintsForm.value.complaints[0] - ).not.toEqual({ - chiefComplaint: { - chiefComplaintID: 1, - chiefComplaint: 'Abdominal Bloating', - }, - chiefComplaintID: null, - beneficiaryRegID: 7416, - benVisitID: 863, - providerServiceMapID: 1320, - duration: 12, - unitOfDuration: 'Hours', - description: 'hghgjhgjh', - createdBy: null, - }); - }); - - it('should remove ChiefComplaintsForm when total complaints is more than one and remove button is clicked', inject( - [MasterdataService], - masterdataService => { - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data + confirm = TestBed.inject(ConfirmationService); + tracking = TestBed.inject(AmritTrackingService); + utils = new VisitDetailUtils( + TestBed.inject(FormBuilder), + TestBed.inject(SessionStorageService) + ); + component.patientChiefComplaintsForm = new FormGroup({ + complaints: new FormArray([utils.createPatientChiefComplaintsForm()]), + }); + spyOn(console, 'log'); + component.ngOnInit(); + nurseMasterData$.next({ chiefComplaintMaster: masterList() }); + }); + + afterEach(() => { + environment.isMMUOfflineSync = originalOffline; + component.ngOnDestroy(); + }); + + const complaints = () => + component.patientChiefComplaintsForm.controls['complaints'] as FormArray; + const addRowAfter = (first: any) => { + complaints().at(0).patchValue({ chiefComplaint: first }); + component.addCheifComplaint(); + }; + + it('ngOnInit sets language, masters, beneficiary and clears provision', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.chiefComplaintMaster).toEqual(masterList()); + expect(component.chiefComplaintTemporarayList[0]).toEqual(masterList()); + expect(component.beneficiary).toEqual({ age: '2 years' }); + expect(nurse.clearNCDScreeningProvision).toHaveBeenCalled(); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + expect(component.getCheifComplaints()?.length).toBe(1); + }); + + it('getCheifComplaints returns null without complaints array', () => { + component.patientChiefComplaintsForm = new FormGroup({}); + expect(component.getCheifComplaints()).toBeNull(); + }); + + describe('view mode', () => { + const load = (list: any[]) => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: { BenChiefComplaints: list } }) + ); + component.mode = 'view'; + nurseMasterData$.next({ chiefComplaintMaster: masterList() }); + }; + + it('loads complaints; fever sets NCD temp and provisional diagnosis', () => { + load([{ chiefComplaint: 'Fever' }]); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.benChiefComplaints).toEqual([ + { chiefComplaint: 'Fever' }, + ]); + expect(component.dataSource.data.length).toBe(1); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(true); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('offline sync: respiratory complaint enables lung assessment', () => { + environment.isMMUOfflineSync = true; + load([{ chiefComplaint: 'Dry cough' }]); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(true); + }); + + it('offline sync: non-respiratory complaint keeps lung assessment off', () => { + environment.isMMUOfflineSync = true; + load([{ chiefComplaint: 'Headache' }]); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('empty complaints disable provision', () => { + load([]); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(false); + }); + + it('ignores non-200 or null responses', () => { + doctor.getVisitComplaintDetails.and.returnValue(of(null)); + component.getChiefComplaints('b', 'v'); + doctor.getVisitComplaintDetails.and.returnValue(of({ statusCode: 500 })); + component.getChiefComplaints('b', 'v'); + expect(nurse.setNCDTemp).not.toHaveBeenCalled(); + }); + }); + + it('getSCTid patches concept id on success only', () => { + master.getSnomedCTRecord.and.returnValue( + of({ statusCode: 200, data: { conceptID: 'C1' } }) + ); + component.getSCTid({ chiefComplaint: 'Fever' }, 0); + expect(master.getSnomedCTRecord).toHaveBeenCalledWith('Fever'); + expect(complaints().at(0).value.conceptID).toBe('C1'); + master.getSnomedCTRecord.and.returnValue(of({ statusCode: 500 })); + component.getSCTid({ chiefComplaint: 'Fever' }, 0); + expect(complaints().at(0).value.conceptID).toBe('C1'); + }); + + it('onInputDuration toggles unit field', () => { + const f = complaints().at(0); + f.get('duration')?.enable(); + f.patchValue({ duration: 2 }); + component.onInputDuration(f); + expect(f.get('unitOfDuration')?.enabled).toBeTrue(); + f.patchValue({ duration: null, unitOfDuration: 'Days' }); + component.onInputDuration(f); + expect(f.get('unitOfDuration')?.disabled).toBeTrue(); + expect(f.get('unitOfDuration')?.value).toBeNull(); + }); + + it('reEnterChiefComplaint enables or resets dependent fields', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: fever }); + component.reEnterChiefComplaint(f); + expect(f.get('duration')?.enabled).toBeTrue(); + expect(f.get('description')?.enabled).toBeTrue(); + f.patchValue({ chiefComplaint: null, duration: 3, description: 'x' }); + component.reEnterChiefComplaint(f); + expect(f.get('duration')?.disabled).toBeTrue(); + expect(f.get('description')?.disabled).toBeTrue(); + expect(f.get('duration')?.value).toBeNull(); + }); + + describe('filterComplaints', () => { + it('selects complaint, removes it from other lists and sets fever flags', () => { + addRowAfter(cough); + component.filterComplaints(fever, 0); + expect(component.selectedChiefComplaintList[0]).toBe(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(fever); + expect(component.chiefComplaintTemporarayList[0]).toContain(fever); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(true); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + }); + + it('re-adds the previous selection to other lists when changed', () => { + addRowAfter(cough); + component.filterComplaints(fever, 0); + component.filterComplaints(headache, 0); + expect(component.chiefComplaintTemporarayList[1]).toContain(fever); + expect(component.chiefComplaintTemporarayList[1]).not.toContain(headache); + expect(component.selectedChiefComplaintList[0]).toBe(headache); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + }); + + it('offline sync flags lung assessment for cough', () => { + environment.isMMUOfflineSync = true; + component.filterComplaints(cough, 0); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(true); + }); + + it('unknown complaint with no selection disables provision', () => { + component.filterComplaints({ chiefComplaint: 'Fe' }, 0); + expect(component.selectedChiefComplaintList.length).toBe(0); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(false); + }); + }); + + it('addCheifComplaint adds a row with a list excluding chosen complaints', () => { + complaints().at(0).patchValue({ chiefComplaint: fever }); + component.addCheifComplaint(); + expect(complaints().length).toBe(2); + expect(component.chiefComplaintTemporarayList[1]).toEqual([ + cough, + headache, + ]); + }); + + describe('removeCheifComplaint', () => { + it('removes a row, restores complaint to other lists and resets flags', () => { + addRowAfter(fever); + component.filterComplaints(fever, 0); + component.removeCheifComplaint(0, complaints().at(0)); + expect(confirm.confirm).toHaveBeenCalledWith( + 'warn', + LANGUAGE_EN.alerts.info.warn ); - const complaint = component.patientChiefComplaintsForm.controls[ - 'complaints' - ] as FormArray; - component.addCheifComplaint(); - component.addCheifComplaint(); - complaint.controls[0].markAsDirty(); - complaint.controls[1].markAsDirty(); - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('#removeBtn0')); - de.triggerEventHandler('click', null); - // component.removeCheifComplaint(1, complaint.controls[1] as FormGroup); - expect(complaint.value.length).toBe(1); - } - )); - - it('should add ChiefComplaintsForm when add button is clicked', inject( - [MasterdataService], - masterdataService => { - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data + expect(complaints().length).toBe(1); + expect(component.patientChiefComplaintsForm.dirty).toBeTrue(); + expect(component.selectedChiefComplaintList[0]).toBeNull(); + expect(component.chiefComplaintTemporarayList[1]).toContain(fever); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(true); + }); + + it('resets the last remaining row instead of removing it', () => { + const f = complaints().at(0); + f.patchValue({ createdBy: 'x' }); + component.removeCheifComplaint(0, f); + expect(complaints().length).toBe(1); + expect(f.value.createdBy).toBeNull(); + }); + + it('offline sync keeps lung assessment when a respiratory complaint remains', () => { + environment.isMMUOfflineSync = true; + addRowAfter(fever); + complaints().at(0).patchValue({ chiefComplaint: null }); + component.filterComplaints(cough, 1); + component.selectedChiefComplaintList[0] = fever; + component.removeCheifComplaint(0, complaints().at(0)); + expect(nurse.setNCDTemp).toHaveBeenCalledWith(false); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(true); + }); + + it('when not confirmed only re-publishes flags', () => { + confirm.confirm.and.returnValue(of(false)); + component.removeCheifComplaint(0, complaints().at(0)); + expect(complaints().length).toBe(1); + expect(nurse.setNCDTemp).not.toHaveBeenCalled(); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + expect(nurse.setNCDScreeningProvision).toHaveBeenCalledWith(false); + }); + }); + + describe('validateDuration', () => { + it('alerts and clears when duration exceeds age', () => { + const f = complaints().at(0); + f.get('duration')?.enable(); + f.get('unitOfDuration')?.enable(); + f.patchValue({ duration: 5, unitOfDuration: 'Years' }); + component.validateDuration(f); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.durationGreaterThanAge ); - const complaint = component.patientChiefComplaintsForm.controls[ - 'complaints' - ] as FormArray; - const de = fixture.debugElement.query(By.css('#addBtn0')); - de.triggerEventHandler('click', null); - fixture.detectChanges(); - expect(complaint.value.length).toBe(2); - } - )); - - it('should get ChiefComplaintsDetails when mode is view', async( - inject( - [DoctorService, MasterdataService], - (doctorService, masterdataService) => { - component.mode = String('view'); - spyOn(component, 'getChiefComplaints').and.callThrough(); - spyOn(doctorService, 'getVisitComplaintDetails').and.returnValue( - Observable.of(data.generalOPDVisitDetails) - ); - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data - ); - expect(component.getChiefComplaints).toHaveBeenCalled(); - expect(doctorService.getVisitComplaintDetails).toHaveBeenCalled(); - } - ) - )); - - it('should patch ChiefComplaintsDetails to form when mode is view', async( - inject( - [DoctorService, MasterdataService], - (doctorService, masterdataService) => { - spyOn(doctorService, 'getVisitComplaintDetails').and.returnValue( - Observable.of(data.generalOPDVisitDetails) - ); - component.mode = String('view'); - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data - ); - fixture.detectChanges(); - expect( - component.patientChiefComplaintsForm.value.complaints.length - ).toBe(2); - } - ) - )); + expect(f.value.duration).toBeNull(); + expect(f.value.unitOfDuration).toBeNull(); + }); + + it('accepts duration within age or incomplete input', () => { + const f = complaints().at(0); + f.get('duration')?.enable(); + f.get('unitOfDuration')?.enable(); + f.patchValue({ duration: 3, unitOfDuration: 'Days' }); + component.validateDuration(f); + f.patchValue({ duration: 3, unitOfDuration: null }); + component.validateDuration(f); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(f.value.duration).toBe(3); + }); + }); + + it('displayChiefComplaint returns name or undefined', () => { + expect(component.displayChiefComplaint(fever)).toBe('Fever'); + expect(component.displayChiefComplaint(null)).toBeUndefined(); + }); + + describe('suggestChiefComplaintList', () => { + it('string input filters suggestions and enables fields', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: 'he' }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([headache]); + expect(f.get('duration')?.enabled).toBeTrue(); + }); + + it('object input filters by its name', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: cough }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([cough]); + expect(f.get('description')?.enabled).toBeTrue(); + }); + + it('no-match string resets the form', () => { + const f = complaints().at(0); + f.patchValue({ chiefComplaint: 'zzz' }); + component.suggestChiefComplaintList(f, 0); + expect(component.suggestedChiefComplaintList[0]).toEqual([]); + expect(f.value.chiefComplaint).toBeNull(); + }); + + it('empty input disables and resets dependent fields', () => { + const f = complaints().at(0); + f.get('duration')?.enable(); + component.suggestedChiefComplaintList[0] = [fever]; + component.suggestChiefComplaintList(f, 0); + expect(f.get('duration')?.disabled).toBeTrue(); + expect(f.get('unitOfDuration')?.disabled).toBeTrue(); + expect(f.get('description')?.disabled).toBeTrue(); + }); + }); + + it('sortChiefComplaintList sorts by name', () => { + const list = [headache, fever, cough, { ...fever }]; + component.sortChiefComplaintList(list); + expect(list.map(x => x.chiefComplaint)).toEqual([ + 'Cough', + 'Fever', + 'Fever', + 'Headache', + ]); + }); + + it('checkComplaintFormValidity is false only when all fields filled', () => { + expect( + component.checkComplaintFormValidity({ + value: { chiefComplaint: fever, duration: 2, unitOfDuration: 'Days' }, + }) + ).toBeFalse(); + expect( + component.checkComplaintFormValidity({ + value: { chiefComplaint: fever, duration: 2 }, + }) + ).toBeTrue(); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('duration'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'duration', + 'Chief Complaints' + ); + }); + + it('ngOnDestroy unsubscribes all subscriptions', () => { + const subs = ['a', 'b', 'c'].map(n => ({ + unsubscribe: jasmine.createSpy(n), + })); + component.nurseMasterDataSubscription = subs[0]; + component.getChiefComplaintDetails = subs[1]; + component.beneficiaryDetailSubscription = subs[2]; + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + component.nurseMasterDataSubscription = null; + component.getChiefComplaintDetails = null; + component.beneficiaryDetailSubscription = null; + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/contact-history/contact-history.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/contact-history/contact-history.component.spec.ts index fe8e5af4..520f8d06 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/contact-history/contact-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/contact-history/contact-history.component.spec.ts @@ -20,27 +20,185 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, Subject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; import { ContactHistoryComponent } from './contact-history.component'; describe('ContactHistoryComponent', () => { let component: ContactHistoryComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let nurse: any; + let session: any; + let nurseMaster$: BehaviorSubject; + let listen$: Subject; + + const contactMaster = [ + { contactHistory: 'Close contact' }, + { contactHistory: 'Healthcare worker' }, + { contactHistory: 'None of the above' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + nurseMaster$ = new BehaviorSubject(null); + listen$ = new Subject(); + master = autoSpy(MasterdataService, { nurseMasterData$: nurseMaster$ }); + master.listen.and.returnValue(listen$.asObservable()); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ContactHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(ContactHistoryComponent, ''); fixture = TestBed.createComponent(ContactHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.patientCovidForm = new FormGroup({ + contactStatus: new FormControl([]), + }); + session = TestBed.inject(SessionStorageService); + spyOn(console, 'log'); + component.ngOnInit(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('ngOnInit sets language and resets contact in session', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(session.setItem).toHaveBeenCalledWith('contact', 'null'); + expect(component.contactArray).toBeUndefined(); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('maps contact history master data', () => { + nurseMaster$.next({ covidContactHistoryMaster: contactMaster }); + expect(component.contactArray).toBe(contactMaster); + expect(component.contactList).toEqual([ + 'Close contact', + 'Healthcare worker', + 'None of the above', + ]); + expect(component.contactData).toEqual(component.contactList); + }); + + describe('onSymptomFilterClick (via masterdata listen)', () => { + it('makes contact optional when all symptoms selected', () => { + session.store.set('allSymptom', 'true'); + listen$.next('true'); + expect(component.allSymp).toBe('true'); + expect(component.contactReqiured).toBe('false'); + }); + + it('makes contact required otherwise', () => { + session.store.set('allSymptom', 'false'); + listen$.next('false'); + expect(component.contactReqiured).toBe('true'); + }); + }); + + describe('contactSelected', () => { + beforeEach(() => + nurseMaster$.next({ covidContactHistoryMaster: contactMaster }) + ); + + it('limits to None of the above when chosen', () => { + component.patientCovidForm.patchValue({ + contactStatus: ['None of the above'], + }); + component.contactSelected(); + expect(component.contactData).toEqual(['None of the above']); + expect(component.cont).toBe('false'); + expect(nurse.filter).toHaveBeenCalledWith('false'); + }); + + it('excludes None of the above when other contacts chosen', () => { + component.patientCovidForm.patchValue({ + contactStatus: ['Close contact'], + }); + component.contactSelected(); + expect(component.contactData).toEqual([ + 'Close contact', + 'Healthcare worker', + ]); + expect(component.cont).toBe('true'); + expect(nurse.filter).toHaveBeenCalledWith('true'); + }); + + it('restores full list when nothing selected', () => { + component.contactSelected(); + expect(component.contactData).toBe(component.contactList); + expect(component.cont).toBe('null'); + expect(nurse.filter).toHaveBeenCalledWith('null'); + }); + }); + + describe('ngOnChanges', () => { + it('loads contact status in view mode', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ + statusCode: 200, + data: { covidDetails: { contactStatus: ['Close contact'] } }, + }) + ); + component.mode = 'view'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.contactStatus).toEqual(['Close contact']); + expect(component.contactResponseList).toEqual(['Close contact']); + expect(component.contactFlag).toBeTrue(); + }); + + it('ignores non-200 responses', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 5000, data: null }) + ); + component.getContactDetails('b1', 'v1'); + expect(component.contactFlag).toBeFalse(); + }); + + it('does not fetch outside view mode', () => { + component.mode = 'add'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', () => { + const a = jasmine.createSpyObj('a', ['unsubscribe']); + const b = jasmine.createSpyObj('b', ['unsubscribe']); + component.contactHistoryMasterData = a; + component.covidContactHistory = b; + component.ngOnDestroy(); + expect(a.unsubscribe).toHaveBeenCalled(); + expect(b.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/covid-vaccination-status/covid-vaccination-status.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/covid-vaccination-status/covid-vaccination-status.component.spec.ts index 56163e8c..97f069d1 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/covid-vaccination-status/covid-vaccination-status.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/covid-vaccination-status/covid-vaccination-status.component.spec.ts @@ -20,27 +20,299 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { + BeneficiaryDetailsService, + ConfirmationService, +} from 'src/app/app-modules/core/services'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; import { CovidVaccinationStatusComponent } from './covid-vaccination-status.component'; describe('CovidVaccinationStatusComponent', () => { let component: CovidVaccinationStatusComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let nurse: any; + let confirm: any; + let beneficiary$: BehaviorSubject; + + const buildForm = () => + new FormGroup({ + covidVSID: new FormControl(null), + ageGroup: new FormControl(null), + isApplicableForVaccine: new FormControl(null), + vaccineStatus: new FormControl(null), + vaccineTypes: new FormControl(null), + doseTaken: new FormControl(null), + }); + const ctrl = (n: string) => component.covidVaccineStatusForm.controls[n]; + const masterOk = { + statusCode: 200, + data: { doseType: [{ id: 1 }], vaccineType: [{ id: 2 }] }, + }; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + beneficiary$ = new BehaviorSubject(null); + master = autoSpy(MasterdataService); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CovidVaccinationStatusComponent], + providers: [ + ...commonTestProviders({ session: { beneficiaryRegID: 'b1' } }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: beneficiary$ }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(CovidVaccinationStatusComponent, ''); fixture = TestBed.createComponent(CovidVaccinationStatusComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.covidVaccineStatusForm = buildForm(); + confirm = TestBed.inject(ConfirmationService); + spyOn(console, 'log'); + }); + + describe('ngOnInit', () => { + it('resets doctor flags, sets language, loads masters without beneficiary', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue(of(masterOk)); + component.ngOnInit(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + expect(doctor.covidVaccineAgeGroup).toBeNull(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.doseTypeList).toEqual([{ id: 1 }]); + expect(component.vaccineTypeList).toEqual([{ id: 2 }]); + expect(master.getPreviousCovidVaccinationDetails).not.toHaveBeenCalled(); + }); + + it('marks under-12 beneficiaries not applicable', () => { + beneficiary$.next({ ageVal: 8 }); + component.ngOnInit(); + expect(ctrl('ageGroup').value).toBe('<12 years'); + expect(ctrl('isApplicableForVaccine').value).toBe( + 'Not Applicable for Vaccination' + ); + expect(ctrl('ageGroup').disabled).toBeTrue(); + expect(ctrl('isApplicableForVaccine').disabled).toBeTrue(); + expect(doctor.covidVaccineAgeGroup).toBe('<12 years'); + expect(component.enableVaccinationStatusFields).toBeFalse(); + }); + + it('marks 12+ beneficiaries applicable and loads previous details (YES)', () => { + beneficiary$.next({ ageVal: 30 }); + master.getVaccinationTypeAndDoseMaster.and.returnValue(of(masterOk)); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ + statusCode: 200, + data: { + covidVSID: 5, + vaccineStatus: 'YES', + covidVaccineTypeID: 2, + doseTypeID: 1, + }, + }) + ); + component.ngOnInit(); + expect(ctrl('ageGroup').value).toBe('>=12 years'); + expect(ctrl('isApplicableForVaccine').value).toBe( + 'Applicable for Vaccination' + ); + expect(component.enableVaccinationStatusFields).toBeTrue(); + expect(master.getPreviousCovidVaccinationDetails).toHaveBeenCalledWith( + 'b1' + ); + expect(ctrl('covidVSID').value).toBe(5); + expect(ctrl('vaccineTypes').value).toBe(2); + expect(ctrl('doseTaken').value).toBe(1); + expect(component.enableVaccineTypeAndDoseTakenFlag).toBeTrue(); + expect(component.enableSaveButton).toBeTrue(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + }); + + it('loads previous details with NO status without type/dose', () => { + beneficiary$.next({ ageVal: 30 }); + master.getVaccinationTypeAndDoseMaster.and.returnValue(of(masterOk)); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: { covidVSID: 6, vaccineStatus: 'NO' } }) + ); + component.ngOnInit(); + expect(ctrl('vaccineStatus').value).toBe('NO'); + expect(ctrl('vaccineTypes').value).toBeNull(); + expect(component.enableVaccineTypeAndDoseTakenFlag).toBeFalse(); + expect(component.enableSaveButton).toBeTrue(); + }); + + it('ignores previous details without covidVSID or non-200', () => { + beneficiary$.next({ ageVal: 30 }); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getPreviousCovidVaccinationDetails(); + expect(ctrl('covidVSID').value).toBeNull(); + master.getPreviousCovidVaccinationDetails.and.returnValue( + of({ statusCode: 5000, data: { covidVSID: 1 } }) + ); + component.getPreviousCovidVaccinationDetails(); + expect(ctrl('covidVSID').value).toBeNull(); + }); + + it('logs previous-details errors', () => { + master.getPreviousCovidVaccinationDetails.and.returnValue( + throwingObs({ errorMessage: 'bad' }) + ); + component.getPreviousCovidVaccinationDetails(); + expect(console.log).toHaveBeenCalledWith('error', 'bad'); + }); + }); + + describe('getVaccinationTypeAndDoseMaster', () => { + it('ignores 200 without data', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 200, data: null }) + ); + component.getVaccinationTypeAndDoseMaster(); + expect(component.doseTypeList).toEqual([]); + }); + + it('ignores non-200', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + of({ statusCode: 5000, data: masterOk.data }) + ); + component.getVaccinationTypeAndDoseMaster(); + expect(component.vaccineTypeList).toEqual([]); + }); + + it('logs errors', () => { + master.getVaccinationTypeAndDoseMaster.and.returnValue( + throwingObs({ errorMessage: 'down' }) + ); + component.getVaccinationTypeAndDoseMaster(); + expect(console.log).toHaveBeenCalledWith('error', 'down'); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', () => { + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + describe('setIsApplicable', () => { + it('handles <12 years', () => { + ctrl('ageGroup').setValue('<12 years'); + ctrl('vaccineStatus').setValue('YES'); + component.setIsApplicable(); + expect(ctrl('vaccineStatus').value).toBeNull(); + expect(ctrl('isApplicableForVaccine').value).toBe( + 'Not Applicable for Vaccination' + ); + expect(component.enableVaccinationStatusFields).toBeFalse(); + expect(doctor.enableCovidVaccinationButton).toBeTrue(); + expect(doctor.covidVaccineAgeGroup).toBe('<12 years'); + }); + + it('handles >=12 years', () => { + ctrl('ageGroup').setValue('>=12 years'); + component.setIsApplicable(); + expect(ctrl('isApplicableForVaccine').value).toBe( + 'Applicable for Vaccination' + ); + expect(component.enableVaccinationStatusFields).toBeTrue(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + expect(component.enableSaveButton).toBeTrue(); + }); + }); + + describe('enableVaccineTypeAndDoseTaken', () => { + it('enables type/dose on YES', () => { + ctrl('vaccineStatus').setValue('YES'); + ctrl('doseTaken').setValue(1); + component.enableVaccineTypeAndDoseTaken(); + expect(ctrl('doseTaken').value).toBeNull(); + expect(component.enableVaccineTypeAndDoseTakenFlag).toBeTrue(); + expect(component.enableSaveButton).toBeTrue(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + }); + + it('allows save on NO', () => { + ctrl('vaccineStatus').setValue('NO'); + component.enableVaccineTypeAndDoseTaken(); + expect(component.enableVaccineTypeAndDoseTakenFlag).toBeFalse(); + expect(component.enableSaveButton).toBeFalse(); + expect(doctor.enableCovidVaccinationButton).toBeTrue(); + }); + }); + + describe('enableSaveButtonInForm', () => { + it('enables saving when type and dose chosen', () => { + ctrl('vaccineTypes').setValue(1); + ctrl('doseTaken').setValue(2); + component.enableSaveButtonInForm(); + expect(component.enableSaveButton).toBeFalse(); + expect(doctor.enableCovidVaccinationButton).toBeTrue(); + }); + + it('disables saving when dose missing', () => { + ctrl('vaccineTypes').setValue(1); + component.enableSaveButtonInForm(); + expect(component.enableSaveButton).toBeTrue(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + }); + }); + + describe('saveBenCovidVaccinationDetails', () => { + beforeEach(() => component.assignSelectedLanguage()); + + it('alerts success and stores covidVSID', () => { + nurse.saveBenCovidVaccinationDetails.and.returnValue( + of({ statusCode: 200, data: { covidVSID: 11 } }) + ); + component.covidVaccineStatusForm.markAsDirty(); + component.saveBenCovidVaccinationDetails(); + expect(nurse.saveBenCovidVaccinationDetails).toHaveBeenCalledWith( + component.covidVaccineStatusForm + ); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.covidVaccinationDetailsSaved, + 'success' + ); + expect(ctrl('covidVSID').value).toBe(11); + expect(component.covidVaccineStatusForm.pristine).toBeTrue(); + expect(doctor.enableCovidVaccinationButton).toBeFalse(); + }); + + it('alerts error message on failure status', () => { + nurse.saveBenCovidVaccinationDetails.and.returnValue( + of({ statusCode: 5000, errorMessage: 'nope' }) + ); + component.saveBenCovidVaccinationDetails(); + expect(confirm.alert).toHaveBeenCalledWith('nope', 'error'); + }); + + it('alerts on http error', () => { + nurse.saveBenCovidVaccinationDetails.and.returnValue(throwingObs('err')); + component.saveBenCovidVaccinationDetails(); + expect(confirm.alert).toHaveBeenCalledWith('err', 'error'); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/diseaseconfirmation/diseaseconfirmation.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/diseaseconfirmation/diseaseconfirmation.component.spec.ts index c2b0701c..e32864ad 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/diseaseconfirmation/diseaseconfirmation.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/diseaseconfirmation/diseaseconfirmation.component.spec.ts @@ -20,27 +20,374 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; +import { IdrsscoreService } from '../../shared/services/idrsscore.service'; import { DiseaseconfirmationComponent } from './diseaseconfirmation.component'; describe('DiseaseconfirmationComponent', () => { let component: DiseaseconfirmationComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let nurse: any; + let idrs: any; + let nurseMasterData$: BehaviorSubject; + let params: any; + let session: Record; + + const questions = [ + { DiseaseQuestionType: 'Diabetes' }, + { DiseaseQuestionType: 'Diabetes' }, + { DiseaseQuestionType: 'Hypertension' }, + { DiseaseQuestionType: 'Epilepsy' }, + { DiseaseQuestionType: 'Vision Screening' }, + { DiseaseQuestionType: 'Asthma' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + const build = async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [DiseaseconfirmationComponent], + providers: [ + ...commonTestProviders({ session }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, + { provide: IdrsscoreService, useValue: idrs }, + { provide: ActivatedRoute, useValue: { snapshot: { params } } }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DiseaseconfirmationComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([new FormGroup({})]), + }); + }; + + beforeEach(() => { + nurseMasterData$ = new BehaviorSubject({ IDRSQuestions: questions }); + master = autoSpy(MasterdataService, { nurseMasterData$ }); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService); + idrs = autoSpy(IdrsscoreService); + params = {}; + session = { + visitID: 'v1', + beneficiaryRegID: 'b1', + serviceLineDetails: JSON.stringify({ vanID: 1, parkingPlaceID: 2 }), + }; + spyOn(console, 'log'); + }); + + const rows = () => component.getDiseaseFormArray(); + const names = () => rows().map((r: any) => r.value.diseaseName); + const lastSelected = () => + idrs.setDiseasesSelected.calls.mostRecent().args[0]; + + describe('edit mode (revisit suspected diseases)', () => { + it('builds unique diseases, pre-selects previous confirmed and disables them', async () => { + nurse.getPreviousVisitData.and.returnValue( + of({ + statusCode: 200, + data: { + confirmedDisease: 'Asthma', + isDiabetic: true, + isDefectiveVision: true, + isEpilepsy: true, + isHypertension: true, + }, + }) + ); + await build(); + component.ngOnInit(); + expect(nurse.getPreviousVisitData).toHaveBeenCalledWith({ + benRegID: 'b1', + }); + expect(names()).toEqual([ + 'Diabetes', + 'Hypertension', + 'Epilepsy', + 'Vision Screening', + 'Asthma', + ]); + expect(component.suspect).toEqual([ + 'Asthma', + 'Diabetes', + 'Vision Screening', + 'Epilepsy', + 'Hypertension', + ]); + expect(rows().every((r: any) => r.get('selected').disabled)).toBeTrue(); + expect(idrs.setHypertensionSelected).toHaveBeenCalled(); + expect(idrs.setConfirmedDiabeticSelected).toHaveBeenCalled(); + expect(lastSelected().length).toBe(5); + }); + + it('with no previous data only publishes empty selection', async () => { + nurse.getPreviousVisitData.and.returnValue(of({ statusCode: 500 })); + await build(); + component.ngOnInit(); + expect(rows().length).toBe(0); + expect(lastSelected()).toEqual([]); + }); + + it('does nothing without master data or questions', async () => { + nurseMasterData$.next(null); + await build(); + component.ngOnInit(); + nurseMasterData$.next({ IDRSQuestions: [] }); + expect(nurse.getPreviousVisitData).not.toHaveBeenCalled(); + }); + + it('marks doctor attendant', async () => { + params = { attendant: 'doctor' }; + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + await build(); + component.isDoctor = false; + component.ngOnInit(); + expect(component.attendantType).toBe('doctor'); + expect(component.isDoctor).toBeTrue(); + expect(rows().some((r: any) => r.get('selected').disabled)).toBeFalse(); + }); + }); + + describe('view mode (IDRS details)', () => { + beforeEach(() => { + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: { isDiabetic: true } }) + ); + }); + + it('adds current IDRS confirmed diseases to previous ones', async () => { + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { IDRSDetail: { confirmedDisease: 'Hypertension,Diabetes' } }, + }) + ); + await build(); + component.mode = 'view'; + component.ngOnInit(); + expect(doctor.getIDRSDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.suspect).toEqual(['Diabetes', 'Hypertension']); + const hyper = component.diseaseArray.find( + (d: any) => d.disease === 'Hypertension' + ); + const diab = component.diseaseArray.find( + (d: any) => d.disease === 'Diabetes' + ); + expect(hyper.current).toBeTrue(); + expect(diab.current).toBeFalse(); + expect(rows()[0].get('selected').disabled).toBeTrue(); + expect(rows()[1].get('selected').disabled).toBeFalse(); + expect(lastSelected()).toEqual(['Diabetes', 'Hypertension']); + expect(idrs.setHypertensionSelected).toHaveBeenCalled(); + }); + + it('uses IDRS diseases as current when no previous ones exist; doctor disables all', async () => { + nurse.getPreviousVisitData.and.returnValue( + of({ statusCode: 200, data: { confirmedDisease: null } }) + ); + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { IDRSDetail: { confirmedDisease: 'Epilepsy' } }, + }) + ); + params = { attendant: 'doctor' }; + await build(); + component.mode = 'view'; + // ngOnInit reads the route param only after the (normally async) IDRS + // call is issued; with synchronous mocks, seed it as it would be by then. + component.attendantType = 'doctor'; + component.ngOnInit(); + expect(component.attendantType).toBe('doctor'); + expect(component.suspect).toEqual(['Epilepsy']); + const ep = component.diseaseArray.find( + (d: any) => d.disease === 'Epilepsy' + ); + expect(ep.current).toBeTrue(); + expect(ep.selected).toBeTrue(); + expect(rows().every((r: any) => r.get('selected').disabled)).toBeTrue(); + }); + + it('handles IDRS with no confirmed disease and failed IDRS call', async () => { + doctor.getIDRSDetails.and.returnValue( + of({ + statusCode: 200, + data: { IDRSDetail: { confirmedDisease: null } }, + }) + ); + await build(); + component.mode = 'view'; + component.ngOnInit(); + expect(component.suspect).toEqual(['Diabetes']); + expect(rows().length).toBe(5); + + doctor.getIDRSDetails.and.returnValue(of(null)); + component.getIDRSDetailsFrmNurse('v1', 'b1'); + expect(lastSelected()).toEqual(['Diabetes']); + }); + + it('skips IDRS when previous visit fails, and when ids missing', async () => { + nurse.getPreviousVisitData.and.returnValue(of({ statusCode: 500 })); + await build(); + component.mode = 'view'; + component.ngOnInit(); + expect(doctor.getIDRSDetails).not.toHaveBeenCalled(); + }); + + it('does not load when visit id is missing', async () => { + delete session['visitID']; + await build(); + component.mode = 'view'; + component.ngOnInit(); + expect(nurse.getPreviousVisitData).not.toHaveBeenCalled(); + }); + }); + + describe('checked', () => { + beforeEach(async () => { + await build(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([]), + }); + component.addMoreDiseases({ disease: 'Hypertension', selected: true }); + component.addMoreDiseases({ disease: 'Diabetes', selected: false }); + component.addMoreDiseases({ disease: 'Asthma', selected: true }); + }); + + it('checking Hypertension/Diabetes sets flags and publishes selection', () => { + component.checked({ checked: true }, rows()[0]); + expect(idrs.setHypertensionSelected).toHaveBeenCalled(); + expect(idrs.setDiseasesSelected).toHaveBeenCalledWith([ + 'Hypertension', + 'Asthma', + ]); + component.checked({ checked: true }, rows()[1]); + expect(idrs.setConfirmedDiabeticSelected).toHaveBeenCalled(); + }); + + it('unchecking clears flags and reports unchecked disease', () => { + component.checked({ checked: false }, rows()[0]); + expect(idrs.clearHypertensionSelected).toHaveBeenCalled(); + expect(idrs.setUnchecked).toHaveBeenCalledWith('Hypertension'); + component.checked({ checked: false }, rows()[1]); + expect(idrs.clearConfirmedDiabeticSelected).toHaveBeenCalled(); + component.checked({ checked: false }, rows()[2]); + expect(idrs.setUnchecked).toHaveBeenCalledWith('Asthma'); + expect(idrs.setDiseasesSelected).not.toHaveBeenCalled(); + }); + }); + + describe('getDiseasesMasterData', () => { + it('builds disease rows from master questions', async () => { + await build(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([]), + }); + component.getDiseasesMasterData(); + expect(names()).toEqual([ + 'Diabetes', + 'Hypertension', + 'Epilepsy', + 'Vision Screening', + 'Asthma', + ]); + expect(rows().some((r: any) => r.get('selected').disabled)).toBeFalse(); + }); + + it('disables pre-selected rows and ignores empty master', async () => { + await build(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([]), + }); + const sel = { ...component, diseaseArray: [] }; + nurseMasterData$.next(null); + component.getDiseasesMasterData(); + expect(rows().length).toBe(0); + expect(sel).toBeDefined(); + }); + }); + + describe('addToChronicDiseases', () => { + beforeEach(async () => { + await build(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([]), + }); + [ + 'Vision Screening', + 'Diabetes', + 'Epilepsy', + 'Hypertension', + 'Asthma', + ].forEach(d => + component.addMoreDiseases({ disease: d, selected: false }) + ); + }); + + it('selects and disables matching chronic diseases', () => { + component.addToChronicDiseases({ + data: { + isDefectiveVision: true, + isDiabetic: true, + isEpilepsy: true, + isHypertension: true, + }, + }); + const r = rows(); + [0, 1, 2, 3].forEach(i => { + expect(r[i].get('selected').value).toBeTrue(); + expect(r[i].get('selected').disabled).toBeTrue(); + }); + expect(r[4].get('selected').value).toBeFalse(); + expect(r[4].get('selected').disabled).toBeFalse(); + }); + + it('leaves rows untouched when no flags are set', () => { + component.addToChronicDiseases({ data: {} }); + expect(rows().some((r: any) => r.get('selected').disabled)).toBeFalse(); + }); + }); + + it('addToSuspected avoids duplicates', async () => { + await build(); + component.suspect = ['A']; + component.addToSuspected('A'); + component.addToSuspected('B'); + expect(component.suspect).toEqual(['A', 'B']); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('addMoreDiseases with no data adds an empty row', async () => { + await build(); + component.diseaseFormsGroup = new FormGroup({ + diseaseFormsArray: new FormArray([]), + }); + component.addMoreDiseases(null); + // createPatientDiseaseArrayForm reads data.disease, which the default + // placeholder object lacks, so the control value becomes null. + expect(rows()[0].value).toEqual({ + diseaseName: null, + flag: null, + selected: null, + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/investigations/investigations.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/investigations/investigations.component.spec.ts index 95929cb4..72edd952 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/investigations/investigations.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/investigations/investigations.component.spec.ts @@ -20,86 +20,231 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { environment } from 'src/environments/environment'; import { - ReactiveFormsModule, - FormBuilder, - FormGroup, - FormArray, -} from '@angular/forms'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { VisitDetailUtils } from '../../shared/utility'; - -import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../shared/services'; - -import { MasterdataServiceStub } from '../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; - + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; import { InvestigationsComponent } from './investigations.component'; describe('InvestigationsComponent', () => { let component: InvestigationsComponent; let fixture: ComponentFixture; - let debugElement: DebugElement; - let fb: FormBuilder; + let master: any; + let doctor: any; + let nurse: any; + let nurseMaster$: BehaviorSubject; + let rbs$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], + const rbs = { + procedureID: 1, + procedureName: environment.RBSTest, + procedureType: 'Laboratory', + }; + const hb = { + procedureID: 2, + procedureName: 'HB', + procedureType: 'Laboratory', + }; + const xray = { + procedureID: 3, + procedureName: 'X-Ray', + procedureType: 'Radiology', + }; + + beforeEach(async () => { + nurseMaster$ = new BehaviorSubject(null); + rbs$ = new BehaviorSubject(null); + master = autoSpy(MasterdataService, { nurseMasterData$: nurseMaster$ }); + doctor = autoSpy(DoctorService); + nurse = autoSpy(NurseService, { + rbsTestResultCurrent$: rbs$, + rbsTestResultFromDoctorFetch: null, + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [InvestigationsComponent], - schemas: [NO_ERRORS_SCHEMA], providers: [ - ConfirmationService, - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: nurse }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(InvestigationsComponent, ''); fixture = TestBed.createComponent(InvestigationsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + component.patientInvestigationsForm = new FormGroup({ + laboratoryList: new FormControl(null), + }); + spyOn(console, 'log'); + }); - fb = debugElement.injector.get(FormBuilder); - component.patientInvestigationsForm = new VisitDetailUtils( - fb - ).createPatientInvestigationsForm(); + afterEach(() => component.ngOnDestroy()); + it('ngOnInit clears RBS, sets language and filters lab tests', () => { fixture.detectChanges(); + nurseMaster$.next({ procedures: [rbs, hb, xray] }); + expect(nurse.clearRbsInVitals).toHaveBeenCalled(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.selectLabTest).toEqual([rbs, hb]); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ignores master data without procedures', () => { + fixture.detectChanges(); + nurseMaster$.next({}); + expect(component.selectLabTest).toBeUndefined(); }); - it('should call getNurseMasterData on Initialisation', inject( - [MasterdataService], - masterdataService => { - masterdataService.nurseMasterDataSource.next( - data.generalOPDNurseMasterdata.data - ); - spyOn(component, 'getNurseMasterData'); - component.ngOnInit(); - expect(component.getNurseMasterData).toHaveBeenCalled(); - expect(component.selectLabTest).toEqual( - data.generalOPDNurseMasterdata.data.labTests.filter(item => { - return item.isRadiologyImaging !== true; + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + describe('view mode', () => { + beforeEach(() => { + component.mode = 'view'; + }); + + it('fetches investigation and patches lab tests, flagging RBS', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ + statusCode: 200, + data: { + Investigation: { + laboratoryList: [ + { procedureID: 1, procedureName: environment.RBSTest }, + { procedureID: 99, procedureName: 'Unknown' }, + ], + }, + }, }) ); - } - )); + fixture.detectChanges(); + nurseMaster$.next({ procedures: [rbs, hb] }); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.laboratoryList.value).toEqual([rbs]); + expect(nurse.setRbsSelectedInInvestigation).toHaveBeenCalledWith(true); + }); + + it('skips patching when investigation has no lab list', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: { Investigation: {} } }) + ); + fixture.detectChanges(); + nurseMaster$.next({ procedures: [hb] }); + expect(component.laboratoryList.value).toBeNull(); + }); + + it('skips when investigation is missing', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + fixture.detectChanges(); + nurseMaster$.next({ procedures: [hb] }); + expect(component.patientInvestigationDetails).toBeUndefined(); + expect(component.laboratoryList.value).toBeNull(); + }); + + it('ignores non-200 responses', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 5000, data: null }) + ); + fixture.detectChanges(); + nurseMaster$.next({ procedures: [hb] }); + expect(component.patientInvestigationDetails).toBeUndefined(); + }); + }); + + describe('rbsTestValidation', () => { + it('stores RBS result when emitted', () => { + fixture.detectChanges(); + rbs$.next(120); + expect(component.RBSTestScore).toBe(120); + expect(component.RBStestDone).toBeTrue(); + expect(component.rbsTestResultCurrent).toBe(120); + }); + + it('resets current result on null', () => { + fixture.detectChanges(); + rbs$.next(120); + rbs$.next(null); + expect(component.rbsTestResultCurrent).toBeNull(); + }); + }); + + describe('canDisable', () => { + it('is true for RBS test when a current result exists', () => { + component.rbsTestResultCurrent = 100; + expect(component.canDisable(rbs)).toBeTrue(); + }); + + it('is true for RBS test when doctor fetch result exists', () => { + component.rbsTestResultCurrent = null; + nurse.rbsTestResultFromDoctorFetch = 90; + expect(component.canDisable(rbs)).toBeTrue(); + }); + + it('is false for RBS test without any result', () => { + component.rbsTestResultCurrent = null; + expect(component.canDisable(rbs)).toBeFalse(); + }); + + it('is false for non-RBS tests', () => { + component.rbsTestResultCurrent = 100; + expect(component.canDisable(hb)).toBeFalse(); + }); + }); + + describe('checkTestName', () => { + it('marks RBS selected when present', () => { + component.checkTestName({ value: [hb, rbs] }); + expect(component.RBStestDone).toBeTrue(); + expect(nurse.setRbsSelectedInInvestigation.calls.allArgs()).toEqual([ + [false], + [true], + ]); + }); + + it('clears RBS selection when absent', () => { + component.RBStestDone = true; + component.checkTestName({ value: [hb] }); + expect(component.RBStestDone).toBeFalse(); + expect(nurse.setRbsSelectedInInvestigation).toHaveBeenCalledOnceWith( + false + ); + }); + }); + + it('checkInvestigation is a no-op', () => { + expect(component.checkInvestigation([])).toBeUndefined(); + }); + + it('ngOnDestroy unsubscribes everything', () => { + fixture.detectChanges(); + const subs = [1, 2, 3].map(() => + jasmine.createSpyObj('s', ['unsubscribe']) + ); + component.nurseMasterDataSubscription = subs[0]; + component.getInvestigationDetails = subs[1]; + component.rbsTestResultSubscription = subs[2]; + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/symptoms/symptoms.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/symptoms/symptoms.component.spec.ts index 5e98c34e..91ca04fa 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/symptoms/symptoms.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/symptoms/symptoms.component.spec.ts @@ -20,27 +20,178 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; import { SymptomsComponent } from './symptoms.component'; describe('SymptomsComponent', () => { let component: SymptomsComponent; let fixture: ComponentFixture; + let master: any; + let doctor: any; + let session: any; + let nurseMaster$: BehaviorSubject; + + const symptomMaster = [ + { symptoms: 'Fever' }, + { symptoms: 'Cough' }, + { symptoms: 'Breathing Difficulty' }, + { symptoms: 'No Symptoms' }, + ]; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + nurseMaster$ = new BehaviorSubject(null); + master = autoSpy(MasterdataService, { nurseMasterData$: nurseMaster$ }); + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [SymptomsComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: NurseService, useValue: autoSpy(NurseService) }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(SymptomsComponent, ''); fixture = TestBed.createComponent(SymptomsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.patientCovidForm = new FormGroup({ + symptom: new FormControl([]), + }); + session = TestBed.inject(SessionStorageService); + spyOn(console, 'log'); + component.ngOnInit(); + }); + + afterEach(() => component.ngOnDestroy()); + + it('ngOnInit sets language, resets flags and session symptom', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(session.setItem).toHaveBeenCalledWith('symptom', 'null'); + expect(component.disable).toEqual(['false', 'false', 'false', 'false']); + expect(component.checked).toEqual([false, false, false, false]); + expect(component.symptomsList).toEqual([]); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + it('builds the symptom list from master data', () => { + nurseMaster$.next({ covidSymptomsMaster: symptomMaster }); + expect(component.masterData.covidSymptomsMaster).toBe(symptomMaster); + expect(component.symptomsList).toEqual([ + 'Fever', + 'Cough', + 'Breathing Difficulty', + 'No Symptoms', + ]); + expect(component.symptomsArray).toBe(component.symptomsList); + }); + + it('ignores master data without covid symptoms', () => { + nurseMaster$.next({ other: [] }); + expect(component.masterData).toBeUndefined(); + }); + + describe('symptomSelected', () => { + beforeEach(() => nurseMaster$.next({ covidSymptomsMaster: symptomMaster })); + + it('restricts list to No Symptoms when chosen', () => { + component.patientCovidForm.patchValue({ symptom: ['No Symptoms'] }); + component.symptomSelected(); + expect(component.symptomsList).toEqual(['No Symptoms']); + expect(component.answer1).toBe('false'); + expect(master.filter).toHaveBeenCalledWith('false'); + }); + + it('excludes No Symptoms and flags allSymptom for three symptoms', () => { + component.patientCovidForm.patchValue({ + symptom: ['Fever', 'Cough', 'Breathing Difficulty'], + }); + component.symptomSelected(); + expect(component.symptomsList).not.toContain('No Symptoms'); + expect(session.store.get('allSymptom')).toBe('true'); + expect(component.answer1).toBe('true'); + expect(master.filter).toHaveBeenCalledWith('true'); + }); + + it('sets allSymptom false for fewer symptoms', () => { + component.patientCovidForm.patchValue({ symptom: ['Fever'] }); + component.symptomSelected(); + expect(session.store.get('allSymptom')).toBe('false'); + }); + + it('resets list and session when nothing selected', () => { + component.patientCovidForm.patchValue({ symptom: ['Fever'] }); + component.symptomSelected(); + component.patientCovidForm.patchValue({ symptom: [] }); + component.symptomSelected(); + expect(component.symptomsList).toBe(component.symptomsArray); + expect(session.store.get('symptom')).toBe('null'); + expect(session.store.get('allSymptom')).toBe('null'); + expect(master.filter).toHaveBeenCalledWith('null'); + }); + }); + + describe('ngOnChanges', () => { + it('loads covid symptoms in view mode', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ + statusCode: 200, + data: { covidDetails: { symptom: ['Fever'] } }, + }) + ); + component.mode = 'view'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.sympFlag).toBeTrue(); + expect(component.covidSymptoms).toEqual(['Fever']); + expect(component.symptom).toEqual(['Fever']); + }); + + it('ignores responses without covid details', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: { covidDetails: null } }) + ); + component.getHistoryDetails('b1', 'v1'); + expect(component.sympFlag).toBeFalse(); + }); + + it('does not fetch outside view mode', () => { + component.mode = 'add'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngOnDestroy unsubscribes', () => { + const a = jasmine.createSpyObj('a', ['unsubscribe']); + const b = jasmine.createSpyObj('b', ['unsubscribe']); + component.nurseMasterDataSubscription = a; + component.coividSymptomsHistory = b; + component.ngOnDestroy(); + expect(a.unsubscribe).toHaveBeenCalled(); + expect(b.unsubscribe).toHaveBeenCalled(); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/travel-history/travel-history.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/travel-history/travel-history.component.spec.ts index ac354937..51caa877 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/travel-history/travel-history.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/travel-history/travel-history.component.spec.ts @@ -20,27 +20,447 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of, Subject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../../shared/services'; import { TravelHistoryComponent } from './travel-history.component'; describe('TravelHistoryComponent', () => { let component: TravelHistoryComponent; let fixture: ComponentFixture; + let master: any; + let nurse: any; + let doctor: any; + let session: any; + let masterListen: Subject; + let nurseListen: Subject; + let nurseMasterData$: BehaviorSubject; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TravelHistoryComponent], - }).compileComponents(); + const recMaster = Array.from({ length: 15 }, (_, i) => ({ + CovidrecommendationID: i + 1, + recommendation: 'R' + (i + 1), })); - beforeEach(() => { + const buildForm = () => + new FormGroup({ + travelStatus: new FormControl(null), + travelList: new FormArray([]), + recommendation: new FormArray([]), + suspectedStatusUI: new FormControl(null), + modeOfTravelDomestic: new FormControl(null), + fromStateDom: new FormControl(null), + fromDistrictDom: new FormControl(null), + fromSubDistrictDom: new FormControl(null), + toStateDom: new FormControl(null), + toDistrictDom: new FormControl(null), + toSubDistrictDom: new FormControl(null), + modeOfTravelInter: new FormControl(null), + fromCountryInter: new FormControl(null), + fromCityInter: new FormControl(null), + toCountryInter: new FormControl(null), + toCityInter: new FormControl(null), + }); + + beforeEach(async () => { + masterListen = new Subject(); + nurseListen = new Subject(); + nurseMasterData$ = new BehaviorSubject({ + covidRecommendationMaster: recMaster, + }); + master = autoSpy(MasterdataService, { nurseMasterData$ }); + master.listen.and.returnValue(masterListen.asObservable()); + nurse = autoSpy(NurseService); + nurse.listen.and.returnValue(nurseListen.asObservable()); + nurse.getCountryName.and.returnValue( + of({ statusCode: 200, data: [{ countryID: 1 }] }) + ); + nurse.getStateName.and.returnValue( + of({ statusCode: 200, data: [{ stateID: 2 }] }) + ); + nurse.getCityName.and.returnValue(of({ statusCode: 200, data: ['c'] })); + nurse.getDistrictName.and.returnValue(of({ statusCode: 200, data: ['d'] })); + nurse.getSubDistrictName.and.returnValue( + of({ statusCode: 200, data: ['sd'] }) + ); + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [TravelHistoryComponent], + providers: [ + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: MasterdataService, useValue: master }, + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); fixture = TestBed.createComponent(TravelHistoryComponent); component = fixture.componentInstance; - fixture.detectChanges(); + session = TestBed.inject(SessionStorageService); + spyOn(console, 'log'); + component.patientCovidForm = buildForm(); + component.ngOnInit(); + }); + + afterEach(() => component.ngOnDestroy()); + + const recs = () => + (component.patientCovidForm.controls['recommendation'] as FormArray).value; + const travelList = () => + component.patientCovidForm.controls['travelList'] as FormArray; + const answers = (sym: string | null, con: string | null, all?: string) => { + session.store.set('symptom', sym); + session.store.set('contact', con); + if (all !== undefined) session.store.set('allSymptom', all); + }; + + it('ngOnInit loads language, masters, states and countries', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.recommendationMaster).toEqual(recMaster); + expect(component.recommendationTemporarayList[0]).toEqual(recMaster); + expect(component.domestictype).toEqual(['Bus', 'Flight', 'Train', 'Ship']); + expect(component.internationaltype).toEqual(['Flight', 'Ship']); + expect(nurse.getStateName).toHaveBeenCalledWith(1); + expect(component.states).toEqual([{ stateID: 2 }]); + expect(component.countries).toEqual([{ countryID: 1 }]); + }); + + it('ignores master data without recommendations and failed lookups', () => { + component.recommendationMaster = 'keep'; + nurseMasterData$.next({}); + expect(component.recommendationMaster).toBe('keep'); + nurse.getCountryName.and.returnValue(of({ statusCode: 500 })); + nurse.getStateName.and.returnValue(of({ statusCode: 200, data: null })); + component.countries = []; + component.states = []; + component.getCountryNames(); + component.getStateNames(); + expect(component.countries).toEqual([]); + expect(component.states).toEqual([]); + }); + + it('recomputes recommendations on symptom and contact events', () => { + spyOn(component, 'getrecommendedtext'); + masterListen.next('sym'); + nurseListen.next('con'); + expect(component.getrecommendedtext).toHaveBeenCalledTimes(2); + }); + + describe('getrecommendedtext', () => { + const cases: [string, string, string, string, number[]][] = [ + ['yes', 'true', 'true', 'YES', [1, 2, 3, 4, 5]], + ['yes', 'true', 'false', 'YES', [1, 2, 3, 4, 5]], + ['no', 'true', 'true', 'YES', [1, 2, 3, 4, 5]], + ['no', 'false', 'true', 'YES', [6, 7, 8, 9, 10, 11]], + ['yes', 'false', 'true', 'YES', [6, 7, 8, 9, 10, 11]], + ['yes', 'false', 'false', 'YES', [11, 12, 13]], + ['no', 'true', 'false', 'NO', [5, 14]], + ['no', 'false', 'false', 'NO', [11, 15]], + ]; + cases.forEach(([q, a1, a2, status, ids]) => { + it(`travel=${q} symptom=${a1} contact=${a2} -> ${status} ${ids}`, () => { + answers(a1, a2, 'false'); + component.question1 = q; + component.getrecommendedtext(); + expect(component.suspectedStatusUI).toBe(status); + expect(component.travelReqiured).toBe('true'); + const expected = ids.map(i => 'R' + i); + expect(recs()).toEqual([expected]); + expect(component.recommendationText).toBe(expected.join('\n')); + }); + }); + + it('all symptoms flag gives suspected YES with 1-5 when no question answered', () => { + answers(null, null, 'true'); + component.question1 = undefined as any; + component.getrecommendedtext(); + expect(component.travelReqiured).toBe('false'); + expect(component.suspectedStatusUI).toBe('YES'); + expect(recs()).toEqual([['R1', 'R2', 'R3', 'R4', 'R5']]); + }); + + it('clears recommendations when nothing matches, replacing old ones', () => { + answers('true', 'true', 'false'); + component.question1 = 'yes'; + component.getrecommendedtext(); + component.getrecommendedtext(); + expect(recs().length).toBe(1); + answers(null, null, 'false'); + component.question1 = undefined as any; + component.getrecommendedtext(); + expect(recs()).toEqual([]); + expect(component.suspectedStatusUI).toBeNull(); + expect(component.recommendationText).toBeNull(); + }); + }); + + describe('travelStatuschange', () => { + it('yes enables travel and stores flag', () => { + answers('true', 'true', 'false'); + component.travelStatuschange('true'); + expect(session.setItem).toHaveBeenCalledWith('travelstat', 'true'); + expect(component.travelStatus).toBe('true'); + expect(component.disableTravelButton).toBeFalse(); + expect(component.travelSelected).toBeTrue(); + expect(component.question1).toBe('yes'); + expect(component.istravelStatus).toBeTrue(); + expect(recs().length).toBe(1); + }); + + it('no clears travel list and resets domestic/international fields', () => { + travelList().push(new FormControl('Domestic')); + travelList().push(new FormControl('International')); + component.patientCovidForm.patchValue({ + fromStateDom: 1, + toCityInter: 2, + }); + component.istravelModeDomestic = true; + component.istravelModeInternatinal = true; + answers('false', 'false', 'false'); + component.travelStatuschange('false'); + expect(component.question1).toBe('no'); + expect(component.istravelStatus).toBeFalse(); + expect(component.istravelModeDomestic).toBeFalse(); + expect(component.istravelModeInternatinal).toBeFalse(); + expect(travelList().length).toBe(0); + expect(component.fromStateDom).toBeNull(); + expect(component.toCityInter).toBeNull(); + expect(recs()).toEqual([['R11', 'R15']]); + }); + }); + + describe('onChange', () => { + it('checking Domestic/International adds entries and flags', () => { + component.onChange('Domestic', { target: { checked: true } }); + component.onChange('International', { target: { checked: true } }); + expect(travelList().value).toEqual(['Domestic', 'International']); + expect(component.istravelModeDomestic).toBeTrue(); + expect(component.istravelModeInternatinal).toBeTrue(); + }); + + it('unchecking removes the entry and resets its fields', () => { + component.onChange('Domestic', { target: { checked: true } }); + component.onChange('International', { target: { checked: true } }); + component.patientCovidForm.patchValue({ + modeOfTravelDomestic: 'Bus', + modeOfTravelInter: 'Ship', + }); + component.onChange('Domestic', { target: { checked: false } }); + expect(travelList().value).toEqual(['International']); + expect(component.istravelModeDomestic).toBeFalse(); + expect(component.modeOfTravelDomestic).toBeNull(); + component.onChange('International', { target: { checked: false } }); + expect(travelList().value).toEqual([]); + expect(component.istravelModeInternatinal).toBeFalse(); + expect(component.modeOfTravelInter).toBeNull(); + }); + }); + + describe('location lookups', () => { + it('international cities from/to patch country and load cities', () => { + component.getCitiesFromInter(5); + component.getCitiesToInter(6); + expect(nurse.getCityName).toHaveBeenCalledWith(5); + expect(nurse.getCityName).toHaveBeenCalledWith(6); + expect(component.fromCountryInter).toBe(5); + expect(component.toCountryInter).toBe(6); + expect(component.citiesFromInter).toEqual(['c']); + expect(component.citiesToInter).toEqual(['c']); + }); + + it('domestic districts and sub-districts patch and load', () => { + component.GetDistrictsFromDom(1); + component.GetDistrictsToDom(2); + component.GetSubDistrictFromDom(3); + component.getSubDistrictToDom(4); + expect(component.fromStateDom).toBe(1); + expect(component.toStateDom).toBe(2); + expect(component.fromDistrictDom).toBe(3); + expect(component.toDistrictDom).toBe(4); + expect(component.districtsFromDom).toEqual(['d']); + expect(component.districtsToDom).toEqual(['d']); + expect(component.subDistrictsFromDom).toEqual(['sd']); + expect(component.subDistrictsToDom).toEqual(['sd']); + }); + + it('failed lookups keep previous lists', () => { + nurse.getCityName.and.returnValue(of({ statusCode: 500 })); + nurse.getDistrictName.and.returnValue(of({ statusCode: 500 })); + nurse.getSubDistrictName.and.returnValue(of({ statusCode: 500 })); + component.getCitiesFromInter(1); + component.getCitiesToInter(1); + component.GetDistrictsFromDom(1); + component.GetDistrictsToDom(1); + component.GetSubDistrictFromDom(1); + component.getSubDistrictToDom(1); + expect(component.citiesFromInter).toEqual([]); + expect(component.citiesToInter).toEqual([]); + expect(component.districtsFromDom).toEqual([]); + expect(component.districtsToDom).toEqual([]); + expect(component.subDistrictsFromDom).toEqual([]); + expect(component.subDistrictsToDom).toEqual([]); + }); + + it('simple setters patch form values and handler lists', () => { + component.traveldomesticStatuschange('Bus'); + component.travelinternationalStatuschange('Flight'); + component.CitiesFromInter('Paris'); + component.CitiesToInter('Rome'); + component.getVillage(7); + component.getVillageTosubDistrictDom(8); + expect(component.modeOfTravelDomestic).toBe('Bus'); + expect(component.modeOfTravelInter).toBe('Flight'); + expect(component.fromCityInter).toBe('Paris'); + expect(component.toCityInter).toBe('Rome'); + expect(component.fromSubDistrictDom).toBe(7); + expect(component.toSubDistrictDom).toBe(8); + expect(component.recommendation).toEqual([]); + + component.getAllCitySuccessHandelerFromInter(['a']); + component.getAllCitySuccessHandelerToInter(['b']); + component.getAllStatesSuccessHandeler(['s']); + component.SetDistrictsFromDom(['df']); + component.SetDistrictsTomDom(['dt']); + component.getSubDistrictSuccessHandelerFromDom(['sf']); + component.getSubDistrictSuccessHandelerToDom(['st']); + component.getVillageSuccessHandeler(['v']); + expect(component.citiesFromInter).toEqual(['a']); + expect(component.citiesToInter).toEqual(['b']); + expect(component.states).toEqual(['s']); + expect(component.districtsFromDom).toEqual(['df']); + expect(component.districtsToDom).toEqual(['dt']); + expect(component.subDistrictsFromDom).toEqual(['sf']); + expect(component.subDistrictsToDom).toEqual(['st']); + expect(component.villages).toEqual(['v']); + }); + }); + + describe('view mode', () => { + const details = (over: any = {}) => ({ + statusCode: 200, + data: { + covidDetails: { + suspectedStatus: true, + recommendation: [['a', 'b']], + travelStatus: true, + travelList: ['Domestic', 'International'], + modeOfTravelDomestic: 'Bus', + fromStateDom: 1, + fromDistrictDom: 2, + fromSubDistrictDom: 3, + toStateDom: 4, + toDistrictDom: 5, + toSubDistrictDom: 6, + modeOfTravelInter: 'Flight', + fromCountryInter: 7, + fromCityInter: 8, + toCountryInter: 9, + toCityInter: 10, + ...over, + }, + }, + }); + + it('ngOnChanges in view mode loads and patches history', () => { + doctor.getVisitComplaintDetails.and.returnValue(of(details())); + component.mode = 'VIEW'; + component.ngOnChanges(); + expect(component.readTravel).toBeTrue(); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith('b1', 'v1'); + expect(component.suspectedStatusUI).toBe('YES'); + expect(component.recommendationText).toBe('a\nb'); + expect(component.travelStatus).toBe('true'); + expect(component.istravelStatus).toBeTrue(); + expect(component.domtravel).toBeTrue(); + expect(component.intertravel).toBeTrue(); + expect(component.istravelModeDomestic).toBeTrue(); + expect(component.istravelModeInternatinal).toBeTrue(); + const v = component.patientCovidForm.value; + expect(v.modeOfTravelDomestic).toBe('Bus'); + expect(v.fromStateDom).toBe(1); + expect(v.fromDistrictDom).toBe(2); + expect(v.fromSubDistrictDom).toBe(3); + expect(v.toStateDom).toBe(4); + expect(v.toDistrictDom).toBe(5); + expect(v.toSubDistrictDom).toBe(6); + expect(v.modeOfTravelInter).toBe('Flight'); + expect(v.fromCountryInter).toBe(7); + expect(v.fromCityInter).toBe(8); + expect(v.toCountryInter).toBe(9); + expect(v.toCityInter).toBe(10); + expect(nurse.getDistrictName).toHaveBeenCalledWith(1); + expect(nurse.getSubDistrictName).toHaveBeenCalledWith(5); + expect(nurse.getCityName).toHaveBeenCalledWith(9); + }); + + it('not suspected, no travel: patches NO/false and skips travel sections', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of( + details({ + suspectedStatus: false, + travelStatus: false, + travelList: [], + }) + ) + ); + component.getHistoryDetails('b', 'v'); + expect(component.suspectedStatusUI).toBe('NO'); + expect(component.travelStatus).toBe('false'); + expect(component.istravelStatus).toBeFalse(); + expect(component.domtravel).toBeFalse(); + expect(component.intertravel).toBeFalse(); + }); + + it('ignores responses without covid details and non-view mode', () => { + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.getHistoryDetails('b', 'v'); + expect(component.covidHistoryDetails).toBeUndefined(); + doctor.getVisitComplaintDetails.calls.reset(); + component.mode = 'edit'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + }); + + it('travelListStatus does nothing without history', () => { + component.covidHistoryDetails = undefined; + component.travelListStatus(); + expect(component.formArray).toBe(travelList()); + }); + }); + + it('ngOnDestroy unsubscribes open subscriptions', () => { + const a = { unsubscribe: jasmine.createSpy('a') }; + const b = { unsubscribe: jasmine.createSpy('b') }; + component.covidHistory = a; + component.nurseMasterDataSubscription = b; + component.ngOnDestroy(); + expect(a.unsubscribe).toHaveBeenCalled(); + expect(b.unsubscribe).toHaveBeenCalled(); + component.covidHistory = null; + component.nurseMasterDataSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/upload-files/upload-files.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/upload-files/upload-files.component.spec.ts index d2f7df1a..295fa886 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/upload-files/upload-files.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/upload-files/upload-files.component.spec.ts @@ -20,27 +20,346 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + createDialogRefMock, + throwingObs, +} from 'src/testing/test-utils'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { LabService } from 'src/app/app-modules/lab/shared/services'; +import { ViewRadiologyUploadedFilesComponent } from 'src/app/app-modules/core/components/view-radiology-uploaded-files/view-radiology-uploaded-files.component'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { NurseService } from '../../shared/services/nurse.service'; +import { DoctorService } from '../../shared/services/doctor.service'; import { UploadFilesComponent } from './upload-files.component'; describe('UploadFilesComponent', () => { let component: UploadFilesComponent; let fixture: ComponentFixture; + let nurse: any; + let doctor: any; + let lab: any; + let confirm: any; + let dialog: any; + let tracking: any; + + const fileEvent = (files: any[]) => ({ target: { files } }); - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + nurse = autoSpy(NurseService); + doctor = autoSpy(DoctorService); + lab = autoSpy(LabService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [UploadFilesComponent], + providers: [ + ...commonTestProviders({ + session: { + userID: 7, + serviceLineDetails: JSON.stringify({ vanID: 3 }), + }, + }), + { provide: NurseService, useValue: nurse }, + { provide: DoctorService, useValue: doctor }, + { provide: LabService, useValue: lab }, + { + provide: BeneficiaryDetailsService, + useValue: { beneficiaryDetails$: of(null) }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + TestBed.overrideTemplate(UploadFilesComponent, ''); fixture = TestBed.createComponent(UploadFilesComponent); component = fixture.componentInstance; - fixture.detectChanges(); + component.patientFileUploadDetailsForm = new FormGroup({ + fileIDs: new FormControl(null), + }); + confirm = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + tracking = TestBed.inject(AmritTrackingService); + spyOn(console, 'log'); + component.ngOnInit(); + }); + + it('ngOnInit / ngDoCheck set language', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + describe('ngOnChanges', () => { + it('disables selection in view mode without file selection', () => { + component.mode = 'view'; + component.enableFileSelection = false; + component.ngOnChanges(); + expect(component.disableFileSelection).toBeTrue(); + }); + + it('enables NCD screening upload in view mode with file selection', () => { + component.mode = 'view'; + component.enableFileSelection = true; + component.ngOnChanges(); + expect(component.enableForNCDScreening).toBeTrue(); + expect(component.disableFileSelection).toBeFalse(); + }); + + it('enables selection outside view mode', () => { + component.disableFileSelection = true; + component.mode = 'add'; + component.ngOnChanges(); + expect(component.disableFileSelection).toBeFalse(); + }); + }); + + describe('checkExtension', () => { + it('accepts valid extensions case-insensitively', () => { + expect(component.checkExtension({ name: 'a.PDF' })).toBeTrue(); + expect(component.checkExtension({ name: 'a.docx' })).toBeTrue(); + }); + + it('rejects unknown and multi-dot names', () => { + expect(component.checkExtension({ name: 'a.exe' })).toBeFalse(); + expect(component.checkExtension({ name: 'a.b.pdf' })).toBeFalse(); + }); + + it('treats a missing file as valid', () => { + expect(component.checkExtension(null)).toBeTrue(); + }); + }); + + describe('uploadFile', () => { + it('ignores empty file lists', () => { + component.uploadFile(fileEvent([])); + expect(confirm.alert).not.toHaveBeenCalled(); + expect(component.file).toBeUndefined(); + }); + + it('alerts for invalid file name', () => { + component.uploadFile(fileEvent([{ name: '.pdf', size: 10 }])); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.invalidFileName, + 'error' + ); + }); + + it('alerts for invalid extension', () => { + component.uploadFile(fileEvent([{ name: 'virus.exe', size: 10 }])); + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.invalidFileExtensionSupportedFileFormats, + 'error' + ); + }); + + it('alerts when file is too large', () => { + component.uploadFile( + fileEvent([{ name: 'big.pdf', size: 6 * 1000 * 1000 }]) + ); + expect(confirm.alert).toHaveBeenCalledWith( + `${LANGUAGE_EN.fileSizeShouldNotExceed} 5 ${LANGUAGE_EN.mb}`, + 'error' + ); + }); + + it('reads valid files as data URL', () => { + const read = spyOn(FileReader.prototype, 'readAsDataURL'); + const file = new File(['abc'], 'report.pdf'); + component.uploadFile(fileEvent([file])); + expect(read).toHaveBeenCalledWith(file); + expect(confirm.alert).not.toHaveBeenCalled(); + }); + }); + + describe('file objects', () => { + beforeEach(() => { + component.file = { name: 'report.pdf' }; + }); + + it('onLoadFileCallback builds a file manager entry', () => { + component.onLoadFileCallback({ + currentTarget: { result: 'data:application/pdf;base64,QUJD' }, + }); + expect(component.fileObj).toEqual([ + { + fileName: 'report.pdf', + fileExtension: '.pdf', + userID: 7, + fileContent: 'QUJD', + vanID: 3, + isUploaded: false, + }, + ]); + expect(nurse.fileData).toBe(component.fileObj); + }); + + it('assignFileObject handles missing file and content', () => { + component.file = undefined; + component.assignFileObject(undefined); + expect(component.fileObj[0].fileName).toBe(''); + expect(component.fileObj[0].fileExtension).toBe(''); + expect(component.fileObj[0].fileContent).toBe(''); + }); + + it('remove deletes a known file and clears nurse file data', () => { + component.assignFileObject('x,1'); + const f = component.fileObj[0]; + component.remove({ other: true }); + expect(component.fileObj.length).toBe(1); + component.remove(f); + expect(component.fileObj).toEqual([]); + expect(nurse.fileData).toBeNull(); + }); + }); + + describe('saveUploadDetails', () => { + it('stores saved files, alerts and patches file IDs', () => { + lab.saveFile.and.returnValue( + of({ statusCode: 200, data: [{ filePath: 'p1' }, { filePath: 'p2' }] }) + ); + component.fileObj = [{ isUploaded: false }]; + component.saveUploadDetails(component.fileObj); + expect(component.fileIDs).toEqual(['p1', 'p2']); + expect( + component.savedFileData.every((f: any) => f.isUploaded) + ).toBeTrue(); + expect(component.fileObj[0].isUploaded).toBeTrue(); + expect(confirm.alert).toHaveBeenCalledWith( + 'File Uploaded successfully', + 'success' + ); + expect(component.uploadFiles).toEqual(['p1', 'p2']); + expect(nurse.fileData).toBeNull(); + }); + + it('does not store files on non-200', () => { + lab.saveFile.and.returnValue(of({ statusCode: 5000 })); + component.saveUploadDetails([]); + expect(component.savedFileData).toEqual([]); + expect(component.uploadFiles).toEqual([]); + }); + + it('patches empty list when fileIDs is null', () => { + lab.saveFile.and.returnValue(of({ statusCode: 5000 })); + component.fileIDs = null; + component.saveUploadDetails([]); + expect(component.uploadFiles).toEqual([]); + }); + + it('alerts on error', () => { + lab.saveFile.and.returnValue(throwingObs({ errorMessage: 'fail' })); + component.saveUploadDetails([]); + expect(confirm.alert).toHaveBeenCalledWith('fail', 'err'); + }); + }); + + describe('checkForDuplicateUpload', () => { + const info = () => + expect(confirm.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pleaseselectfiletoupload, + 'info' + ); + + it('alerts when fileObj undefined', () => { + component.fileObj = undefined; + component.checkForDuplicateUpload(); + info(); + }); + + it('uploads everything when savedFileData undefined', () => { + spyOn(component, 'saveUploadDetails'); + component.fileObj = [{ isUploaded: false }]; + component.savedFileData = undefined; + component.checkForDuplicateUpload(); + expect(component.saveUploadDetails).toHaveBeenCalledWith( + component.fileObj + ); + }); + + it('alerts when no new files', () => { + component.fileObj = []; + component.checkForDuplicateUpload(); + info(); + }); + + it('uploads only not-yet-uploaded files', () => { + spyOn(component, 'saveUploadDetails'); + const fresh = { isUploaded: false }; + component.fileObj = [{ isUploaded: true }, fresh]; + component.savedFileData = [{ isUploaded: true }]; + component.checkForDuplicateUpload(); + expect(component.saveUploadDetails).toHaveBeenCalledWith([fresh]); + }); + + it('alerts when all pending files match saved state', () => { + spyOn(component, 'saveUploadDetails'); + component.fileObj = [{ isUploaded: true }, { isUploaded: true }]; + component.savedFileData = [{ isUploaded: true }]; + component.checkForDuplicateUpload(); + expect(component.saveUploadDetails).not.toHaveBeenCalled(); + info(); + }); + }); + + describe('viewNurseSelectedFiles', () => { + it('opens dialog and downloads the chosen file', () => { + const ref = createDialogRefMock({ fileName: 'r.pdf' }); + dialog.open.and.returnValue(ref); + doctor.fileIDs = ['p1']; + lab.viewFileContent.and.returnValue(of(new Blob(['x']))); + spyOn(window.URL, 'createObjectURL').and.returnValue('blob:x'); + const click = spyOn(HTMLAnchorElement.prototype, 'click'); + component.viewNurseSelectedFiles(); + expect(dialog.open).toHaveBeenCalledWith( + ViewRadiologyUploadedFilesComponent, + jasmine.objectContaining({ + width: '40%', + data: jasmine.objectContaining({ filesDetails: ['p1'] }), + }) + ); + expect(lab.viewFileContent).toHaveBeenCalledWith({ fileName: 'r.pdf' }); + expect(click).toHaveBeenCalled(); + }); + + it('does nothing when dialog closes without result', () => { + dialog.open.and.returnValue(createDialogRefMock(undefined)); + component.viewNurseSelectedFiles(); + expect(lab.viewFileContent).not.toHaveBeenCalled(); + }); + }); + + describe('triggerLog', () => { + it('clicks the hidden file input on real clicks', () => { + const input = document.createElement('input'); + input.id = 'fileUpload'; + const click = spyOn(input, 'click'); + document.body.appendChild(input); + component.triggerLog({ clientX: 10 }); + expect(click).toHaveBeenCalled(); + document.body.removeChild(input); + }); + + it('ignores keyboard-triggered clicks (clientX 0)', () => { + const spy = spyOn(document, 'getElementById'); + component.triggerLog({ clientX: 0 }); + expect(spy).not.toHaveBeenCalled(); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('Upload'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Upload', + 'Upload Files' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/visit-details.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/visit-details.component.spec.ts index 03864964..9edd77d6 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/visit-details.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/visit-details.component.spec.ts @@ -1,53 +1,48 @@ -/* - * AMRIT – Accessible Medical Records via Integrated Technology - * Integrated EHR (Electronic Health Records) Solution - * - * Copyright (C) "Piramal Swasthya Management and Research Institute" - * - * This file is part of AMRIT. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/. - */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../core/material.module'; - -import { VisitDetailUtils } from '../shared/utility'; - -import { ConfirmationService } from '../../core/services/confirmation.service'; -import { DoctorService } from '../shared/services'; -import { DoctorServiceStub } from '../shared/mocks/doctor-service-stub'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; import { VisitDetailsComponent } from './visit-details.component'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('VisitDetailsComponent', () => { let component: VisitDetailsComponent; let fixture: ComponentFixture; - let debugElement; - let fb; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + + function buildParentForm(): FormGroup { + return new FormGroup({ + patientVisitDetailsForm: new FormGroup({ + visitCategory: new FormControl(''), + }), + covidVaccineStatusForm: new FormGroup({}), + patientChiefComplaintsForm: new FormGroup({}), + patientAdherenceForm: new FormGroup({}), + patientInvestigationsForm: new FormGroup({}), + patientCovidForm: new FormGroup({}), + patientFileUploadDetailsForm: new FormGroup({}), + patientDiseaseForm: new FormGroup({}), + }); + } + + beforeEach(waitForAsync(() => { + mockHttpService = jasmine.createSpyObj('HttpServiceService', [], { + currentLangugae$: new BehaviorSubject({ test: 'language' }), + }); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); - beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, ReactiveFormsModule, MaterialModule], + imports: [ReactiveFormsModule], declarations: [VisitDetailsComponent], providers: [ - ConfirmationService, - { provide: DoctorService, useClass: DoctorServiceStub }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -56,13 +51,7 @@ describe('VisitDetailsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(VisitDetailsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - component.patientVisitForm = new VisitDetailUtils( - fb - ).createPatientVisitForm(); - + component.patientVisitDataForm = buildParentForm(); fixture.detectChanges(); }); @@ -70,38 +59,184 @@ describe('VisitDetailsComponent', () => { expect(component).toBeTruthy(); }); - it('should show Visit Details', () => { - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('patient-visit-details')); - expect(de.nativeElement).toBeTruthy(); + describe('ngOnInit', () => { + it('should extract all sub-forms from the parent form', () => { + expect(component.patientVisitDetailsForm).toBeTruthy(); + expect(component.covidVaccineStatusForm).toBeTruthy(); + expect(component.patientChiefComplaintsForm).toBeTruthy(); + expect(component.patientAdherenceForm).toBeTruthy(); + expect(component.patientInvestigationsForm).toBeTruthy(); + expect(component.patientCovidForm).toBeTruthy(); + expect(component.patientFileUploadDetailsForm).toBeTruthy(); + expect(component.patientDiseaseForm).toBeTruthy(); + }); + + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngOnInit(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); + }); + + it('should call getVisitCategory', () => { + spyOn(component, 'getVisitCategory'); + component.ngOnInit(); + expect(component.getVisitCategory).toHaveBeenCalled(); + }); }); - it('should not show Chief Complaints', () => { - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('patient-chief-complaints')); - expect(de).not.toBeTruthy(); + describe('getVisitCategory', () => { + it('should subscribe to visitCategory valueChanges and call conditionCheck on value change', () => { + spyOn(component, 'conditionCheck'); + const visitCategoryControl = ( + component.patientVisitDataForm.get( + 'patientVisitDetailsForm' + ) as FormGroup + ).controls['visitCategory']; + + visitCategoryControl.setValue('ANC'); + + expect(component.visitCategory).toBe('ANC'); + expect(component.conditionCheck).toHaveBeenCalled(); + }); + + it('should not call conditionCheck when visitCategory is empty/null', () => { + spyOn(component, 'conditionCheck'); + const visitCategoryControl = ( + component.patientVisitDataForm.get( + 'patientVisitDetailsForm' + ) as FormGroup + ).controls['visitCategory']; + + visitCategoryControl.setValue(''); + + expect(component.conditionCheck).not.toHaveBeenCalled(); + }); }); - it('should show Chief Complaints after selected visit category is General OPD', () => { - component.patientVisitForm.controls['patientVisitDetailsForm'].patchValue({ - visitCategory: 'General OPD', + describe('conditionCheck', () => { + it('should call hideAllTab when mode is falsy', () => { + component.mode = ''; + spyOn(component, 'hideAllTab'); + component.visitCategory = 'ANC'; + component.conditionCheck(); + expect(component.hideAllTab).toHaveBeenCalled(); + }); + + it('should not call hideAllTab when mode is set', () => { + component.mode = 'view'; + spyOn(component, 'hideAllTab'); + component.visitCategory = 'ANC'; + component.conditionCheck(); + expect(component.hideAllTab).not.toHaveBeenCalled(); + }); + + it('should set sessionStorage with visitCategory', () => { + component.visitCategory = 'ANC'; + component.conditionCheck(); + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + 'visiCategoryANC', + 'ANC' + ); + }); + + it('should set flags for NCD screening', () => { + component.visitCategory = 'NCD screening'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.enableFileSelection).toBeTrue(); + expect(component.showNcdScreeningVisit).toBeTrue(); + expect(component.hideAll).toBeFalse(); + }); + + it('should set hideAll false for Cancer Screening', () => { + component.visitCategory = 'Cancer Screening'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.hideAll).toBeFalse(); + }); + + it('should set hideAll false for General OPD (QC)', () => { + component.visitCategory = 'General OPD (QC)'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.hideAll).toBeFalse(); + }); + + it('should show ANC visit when visitCategory is ANC', () => { + component.visitCategory = 'ANC'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.showANCVisit).toBeTrue(); + }); + + it('should show NCD care when visitCategory is NCD care', () => { + component.visitCategory = 'NCD care'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.showNCDCare).toBeTrue(); + }); + + it('should show PNC when visitCategory is PNC', () => { + component.visitCategory = 'PNC'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.showPNC).toBeTrue(); + }); + + it('should show PNC when visitCategory is General OPD', () => { + component.visitCategory = 'General OPD'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.showPNC).toBeTrue(); + }); + + it('should show COVID when visitCategory is COVID-19 Screening', () => { + component.visitCategory = 'COVID-19 Screening'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.showCOVID).toBeTrue(); + }); + + it('should set hideAll false for unknown category', () => { + component.visitCategory = 'Unknown Category'; + component.mode = 'view'; + component.conditionCheck(); + expect(component.hideAll).toBeFalse(); }); - fixture.detectChanges(); - const de = fixture.debugElement.query(By.css('patient-chief-complaints')); - expect(de.nativeElement).toBeTruthy(); }); - it('should call conditionCheck when visit category is selected', () => { - spyOn(component, 'conditionCheck'); - component.patientVisitForm.controls['patientVisitDetailsForm'].patchValue({ - visitCategory: 'General OPD', + describe('hideAllTab', () => { + it('should reset all visibility flags', () => { + component.hideAll = true; + component.showANCVisit = true; + component.showNCDCare = true; + component.showPNC = true; + component.showCOVID = true; + component.showNcdScreeningVisit = true; + + component.hideAllTab(); + + expect(component.hideAll).toBeFalse(); + expect(component.showANCVisit).toBeFalse(); + expect(component.showNCDCare).toBeFalse(); + expect(component.showPNC).toBeFalse(); + expect(component.showCOVID).toBeFalse(); + expect(component.showNcdScreeningVisit).toBeFalse(); + }); + }); + + describe('ngDoCheck', () => { + it('should call assignSelectedLanguage', () => { + spyOn(component, 'assignSelectedLanguage'); + component.ngDoCheck(); + expect(component.assignSelectedLanguage).toHaveBeenCalled(); }); - expect(component.conditionCheck).toHaveBeenCalled(); }); - it('should call getVisitCategory On Initialisation', () => { - spyOn(component, 'getVisitCategory'); - component.ngOnInit(); - expect(component.getVisitCategory).toHaveBeenCalled(); + describe('assignSelectedLanguage', () => { + it('should set current_language_set from the language service', () => { + component.assignSelectedLanguage(); + expect(component.current_language_set).toEqual({ test: 'language' }); + }); }); }); diff --git a/src/app/app-modules/nurse-doctor/visit-details/visit-details/visit-details.component.spec.ts b/src/app/app-modules/nurse-doctor/visit-details/visit-details/visit-details.component.spec.ts index e52acf21..82a7593e 100644 --- a/src/app/app-modules/nurse-doctor/visit-details/visit-details/visit-details.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/visit-details/visit-details/visit-details.component.spec.ts @@ -20,154 +20,292 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; import { - async, - inject, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../../core/material.module'; - -import { VisitDetailUtils } from '../../shared/utility'; - -import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { DoctorService, MasterdataService } from '../../shared/services'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { DoctorService, MasterdataService } from '../../shared/services'; +import { PatientVisitDetailsComponent } from './visit-details.component'; -import { MasterdataServiceStub } from '../../shared/mocks/masterdata-service-stub'; -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../core/mocks/beneficiary-details-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Rx'; +describe('PatientVisitDetailsComponent', () => { + let component: PatientVisitDetailsComponent; + let fixture: ComponentFixture; + let master: any; + let doctor: any; + let session: any; + let tracking: any; + let visitMaster$: BehaviorSubject; + let beneficiary$: BehaviorSubject; -import { VisitDetailsComponent } from './visit-details.component'; + const categories = [ + { visitCategory: 'ANC' }, + { visitCategory: 'PNC' }, + { visitCategory: 'NCD screening' }, + { visitCategory: 'Cancer Screening' }, + { visitCategory: 'COVID-19 Screening' }, + { visitCategory: 'General OPD' }, + ]; + const masterData = { + visitReasons: [{ visitReason: 'New Chief Complaint' }], + visitCategories: categories, + }; -describe('VisitDetailsComponent', () => { - let component: VisitDetailsComponent; - let fixture: ComponentFixture; - let debugElement: any; - let fb: any; + const buildForm = () => + new FormGroup({ + visitReason: new FormControl(null), + visitCategory: new FormControl(null), + pregnancyStatus: new FormControl(null), + rCHID: new FormControl(null), + subVisitCategory: new FormControl(null), + }); - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, ReactiveFormsModule, MaterialModule], - declarations: [VisitDetailsComponent], - schemas: [NO_ERRORS_SCHEMA], + beforeEach(async () => { + visitMaster$ = new BehaviorSubject(null); + beneficiary$ = new BehaviorSubject(null); + master = autoSpy(MasterdataService, { + visitDetailMasterData$: visitMaster$, + }); + doctor = autoSpy(DoctorService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [PatientVisitDetailsComponent], providers: [ - { provide: MasterdataService, useClass: MasterdataServiceStub }, - { provide: DoctorService, useClass: DoctorServiceStub }, + ...commonTestProviders({ + session: { visitID: 'v1', beneficiaryRegID: 'b1' }, + }), + { provide: MasterdataService, useValue: master }, + { provide: DoctorService, useValue: doctor }, + { provide: SetLanguageComponent, useValue: {} }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: { beneficiaryDetails$: beneficiary$ }, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(VisitDetailsComponent); + TestBed.overrideTemplate(PatientVisitDetailsComponent, ''); + fixture = TestBed.createComponent(PatientVisitDetailsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + component.patientVisitDetailsForm = buildForm(); + session = TestBed.inject(SessionStorageService); + tracking = TestBed.inject(AmritTrackingService); + spyOn(console, 'log'); + }); + + afterEach(() => component.ngOnDestroy()); - fb = debugElement.injector.get(FormBuilder); - component.patientVisitDetailsForm = new VisitDetailUtils( - fb - ).createPatientVisitDetails(); - fixture.detectChanges(); + it('ngOnInit loads language and subscribes to beneficiary details', () => { + component.ngOnInit(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.beneficiary).toBeUndefined(); + expect(component.beneficiaryDetailsSubscription).toBeTruthy(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('ngDoCheck refreshes the language set', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); - it('should call getVisitReasonAndCategory on Initialisation', () => { - spyOn(component, 'getVisitReasonAndCategory'); - component.ngOnInit(); - expect(component.getVisitReasonAndCategory).toHaveBeenCalled(); + describe('getBenificiaryDetails', () => { + beforeEach(() => component.ngOnInit()); + + it('hides pregnancy status for males and filters ANC/PNC for adult males', () => { + beneficiary$.next({ genderName: 'Male', ageVal: 40 }); + visitMaster$.next(masterData); + expect(component.beneficiaryGender).toBe('Male'); + expect(component.showPregnancyStatus).toBeFalse(); + expect(component.templateVisitReasons).toEqual(masterData.visitReasons); + expect( + component.templateFilterVisitCategories.map((c: any) => c.visitCategory) + ).toEqual([ + 'NCD screening', + 'Cancer Screening', + 'COVID-19 Screening', + 'General OPD', + ]); + expect(component.visitReason).toBe('New Chief Complaint'); + expect(component.visitCategory).toBe('NCD screening'); + }); + + it('hides pregnancy status for females under 19', () => { + beneficiary$.next({ genderName: 'Female', ageVal: 15 }); + visitMaster$.next(masterData); + expect(component.showPregnancyStatus).toBeFalse(); + expect(component.templateFilterVisitCategories).toBe(categories); + expect(component.visitCategory).toBeNull(); + }); + + it('shows pregnancy status for adult females and keeps all categories', () => { + beneficiary$.next({ genderName: 'Female', ageVal: 35 }); + visitMaster$.next(masterData); + expect(component.showPregnancyStatus).toBeTrue(); + expect(component.templateFilterVisitCategories).toEqual(categories); + expect(component.templateFilterVisitCategories).not.toBe(categories); + expect(component.visitCategory).toBe('NCD screening'); + }); + + it('does not auto-select categories in view mode', () => { + component.mode = 'view'; + beneficiary$.next({ genderName: 'Female', ageVal: 35 }); + visitMaster$.next(masterData); + expect(component.templateFilterVisitCategories).toBe(categories); + expect(component.visitReason).toBeNull(); + }); + + it('ignores null master data', () => { + beneficiary$.next({ genderName: 'Female', ageVal: 35 }); + expect(component.templateNurseMasterData).toBeUndefined(); + }); }); - it('should call getBenificiaryDetails on Initialisation', () => { - spyOn(component, 'getBenificiaryDetails'); - component.ngOnInit(); - expect(component.getBenificiaryDetails).toHaveBeenCalled(); + describe('reasonSelected', () => { + beforeEach(() => { + component.templateVisitCategories = categories; + }); + + it('filters screening categories', () => { + component.reasonSelected('Screening'); + expect( + component.templateFilterVisitCategories.map((c: any) => c.visitCategory) + ).toEqual(['NCD screening', 'Cancer Screening', 'COVID-19 Screening']); + }); + + it('filters COVID categories for Pandemic', () => { + component.reasonSelected('Pandemic'); + expect(component.templateFilterVisitCategories).toEqual([ + { visitCategory: 'COVID-19 Screening' }, + ]); + }); + + it('removes ANC/PNC for males', () => { + component.beneficiary = { genderName: 'Male', ageVal: 40 }; + component.reasonSelected('Other'); + expect(component.templateFilterVisitCategories.length).toBe(4); + }); + + it('removes ANC/PNC for children under 12', () => { + component.beneficiary = { genderName: 'Female', ageVal: 8 }; + component.reasonSelected('Other'); + expect( + component.templateFilterVisitCategories.some( + (c: any) => c.visitCategory === 'ANC' + ) + ).toBeFalse(); + }); + + it('keeps a copy of all categories for adult females', () => { + component.beneficiary = { genderName: 'Female', ageVal: 25 }; + component.reasonSelected('Other'); + expect(component.templateFilterVisitCategories).toEqual(categories); + }); }); - it('should show pregnancyStatus for female beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - component.patientVisitDetailsForm.patchValue({ - visitReason: 'FollowUp', - visitCategory: 'General OPD', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#pregnancyStatus')); - expect(component.beneficiary).toEqual(data.femaleBeneficiary); - expect(debugElement).toBeTruthy(); - }) - )); - - it('should not show pregnancyStatus for male beneficiary', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - component.patientVisitDetailsForm.patchValue({ - visitReason: 'FollowUp', - visitCategory: 'General OPD', - }); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#pregnancyStatus')); - expect(component.beneficiary).toEqual(data.maleBeneficiary); - expect(debugElement).not.toBeTruthy(); - }) - )); - - it('should get Visit Reason and Category Master Data', inject( - [MasterdataService], - masterdataService => { - masterdataService.visitDetailMasterDataSource.next( - data.visitReasonAndCategory - ); - expect(component.templateNurseMasterData).toEqual( - data.visitReasonAndCategory - ); - } - )); - - it('should get VisitDetails when mode is view', () => { - spyOn(component, 'getVisitDetails'); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getVisitDetails).toHaveBeenCalled(); + describe('checkCategoryDependent', () => { + it('forces pregnancy status Yes for ANC', () => { + component.patientVisitDetailsForm.patchValue({ rCHID: 'R1' }); + component.checkCategoryDependent('ANC'); + expect(session.setItem).toHaveBeenCalledWith('visiCategoryANC', 'ANC'); + expect(component.templatePregnancyStatus).toEqual(['Yes']); + expect(component.pregnancyStatus).toBe('Yes'); + expect(component.rCHID).toBeNull(); + }); + + it('resets pregnancy options for other categories', () => { + component.patientVisitDetailsForm.patchValue({ pregnancyStatus: 'Yes' }); + component.checkCategoryDependent('PNC'); + expect(component.templatePregnancyStatus).toEqual([ + 'Yes', + 'No', + "Don't Know", + ]); + expect(component.pregnancyStatus).toBeNull(); + }); }); - it('should patch visitDetails to form when mode is view', inject( - [DoctorService], - doctorService => { - localStorage.setItem('visitCategory', 'General OPD'); - spyOn(doctorService, 'getVisitComplaintDetails').and.returnValue( - Observable.of(data.generalOPDVisitDetails) + describe('ngOnChanges / getVisitDetails', () => { + const cases: [string, string][] = [ + ['Cancer Screening', 'benVisitDetails'], + ['General OPD (QC)', 'benVisitDetails'], + ['ANC', 'ANCNurseVisitDetail'], + ['General OPD', 'GOPDNurseVisitDetail'], + ['NCD screening', 'NCDScreeningNurseVisitDetail'], + ['NCD care', 'NCDCareNurseVisitDetail'], + ['PNC', 'PNCNurseVisitDetail'], + ['COVID-19 Screening', 'covid19NurseVisitDetail'], + ]; + cases.forEach(([category, key]) => { + it(`patches the form from ${key} for ${category}`, () => { + session.store.set('visitCategory', category); + doctor.getVisitComplaintDetails.and.returnValue( + of({ + statusCode: 200, + data: { + [key]: { visitReason: 'Follow Up', rCHID: 'R9', files: [7] }, + }, + }) + ); + component.mode = 'view'; + component.ngOnChanges(); + expect(doctor.getVisitComplaintDetails).toHaveBeenCalledWith( + 'b1', + 'v1' + ); + expect(doctor.fileIDs).toEqual([7]); + expect(component.visitReason).toBe('Follow Up'); + expect(component.rCHID).toBe('R9'); + }); + }); + + it('does nothing when response is not 200', () => { + session.store.set('visitCategory', 'ANC'); + doctor.getVisitComplaintDetails.and.returnValue( + of({ statusCode: 5000, data: null }) ); - spyOn(component, 'getVisitDetails').and.callThrough(); - component.mode = new String('view'); + component.getVisitDetails('v1', 'b1'); + expect(component.visitReason).toBeNull(); + expect(doctor.fileIDs).toBeUndefined(); + }); + + it('does nothing for a null response', () => { + doctor.getVisitComplaintDetails.and.returnValue(of(null)); + component.getVisitDetails('v1', 'b1'); + expect(component.visitReason).toBeNull(); + }); + + it('does not fetch when mode is not view', () => { + component.mode = 'add'; component.ngOnChanges(); - expect(doctorService.getVisitComplaintDetails).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVisitDetailsForm.value.visitReason).toEqual( - data.generalOPDVisitDetails.data.GOPDNurseVisitDetail.visitReason - ); - expect(component.patientVisitDetailsForm.value.visitCategory).toEqual( - data.generalOPDVisitDetails.data.GOPDNurseVisitDetail.visitCategory - ); - expect(component.patientVisitDetailsForm.value.pregnancyStatus).toEqual( - data.generalOPDVisitDetails.data.GOPDNurseVisitDetail.pregnancyStatus - ); - expect(component.patientVisitDetailsForm.value.rCHID).toEqual( - data.generalOPDVisitDetails.data.GOPDNurseVisitDetail.rCHID - ); - } - )); + expect(doctor.getVisitComplaintDetails).not.toHaveBeenCalled(); + }); + }); + + it('ngOnDestroy unsubscribes all subscriptions', () => { + const subs = [1, 2, 3].map(() => + jasmine.createSpyObj('sub', ['unsubscribe']) + ); + component.visitCategorySubscription = subs[0]; + component.visitDetailsSubscription = subs[1]; + component.beneficiaryDetailsSubscription = subs[2]; + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('Visit Reason'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Visit Reason', + 'Visit Details' + ); + }); }); diff --git a/src/app/app-modules/nurse-doctor/vitals/cancer-patient-vitals/cancer-patient-vitals.component.spec.ts b/src/app/app-modules/nurse-doctor/vitals/cancer-patient-vitals/cancer-patient-vitals.component.spec.ts index df9cda83..c961b9c5 100644 --- a/src/app/app-modules/nurse-doctor/vitals/cancer-patient-vitals/cancer-patient-vitals.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/vitals/cancer-patient-vitals/cancer-patient-vitals.component.spec.ts @@ -20,27 +20,812 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; import { CancerPatientVitalsComponent } from './cancer-patient-vitals.component'; +import { ConfirmationService } from '../../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { NurseService } from '../../shared/services/nurse.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { IotcomponentComponent } from 'src/app/app-modules/core/components/iotcomponent/iotcomponent.component'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { environment } from 'src/environments/environment'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; + +const CONTROLS = [ + 'height_cm', + 'weight_Kg', + 'waistCircumference_cm', + 'systolicBP_1stReading', + 'diastolicBP_1stReading', + 'systolicBP_2ndReading', + 'diastolicBP_2ndReading', + 'systolicBP_3rdReading', + 'diastolicBP_3rdReading', + 'hbA1C', + 'hemoglobin', + 'bloodGlucose_Fasting', + 'bloodGlucose_Random', + 'bloodGlucose_2HrPostPrandial', + 'sPO2', + 'rbsTestResult', + 'rbsTestRemarks', + 'temperature', + 'pulseRate', +]; + +function buildForm(required = false): FormGroup { + const group: Record = {}; + CONTROLS.forEach( + c => (group[c] = new FormControl(null, required ? Validators.required : [])) + ); + return new FormGroup(group); +} describe('CancerPatientVitalsComponent', () => { let component: CancerPatientVitalsComponent; let fixture: ComponentFixture; + let form: FormGroup; + let doctor: any; + let nurse: any; + let benService: any; + let confirmation: any; + let dialog: any; + let tracking: any; + let route: any; + const lang = LANGUAGE_EN; + const recheck = lang.alerts.info.recheckValue; + + function dialogReturns(result: any) { + dialog.open.and.returnValue({ afterClosed: () => of(result) }); + } - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [CancerPatientVitalsComponent], - }).compileComponents(); - })); + providers: [ + ...commonTestProviders({ + session: { beneficiaryRegID: '101', visitID: '7' }, + }), + { provide: DoctorService, useValue: autoSpy(DoctorService) }, + { + provide: NurseService, + useValue: autoSpy(NurseService, { + rbsSelectedInInvestigation$: new BehaviorSubject(undefined), + rbsTestResultFromDoctorFetch: null, + }), + }, + { + provide: BeneficiaryDetailsService, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: new BehaviorSubject(null), + }), + }, + { provide: SetLanguageComponent, useValue: {} }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { attendant: 'doctor' } } }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(CancerPatientVitalsComponent, '') + .compileComponents(); + + doctor = TestBed.inject(DoctorService); + nurse = TestBed.inject(NurseService); + benService = TestBed.inject(BeneficiaryDetailsService); + confirmation = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + tracking = TestBed.inject(AmritTrackingService); + route = TestBed.inject(ActivatedRoute); + TestBed.inject(SessionStorageService); - beforeEach(() => { fixture = TestBed.createComponent(CancerPatientVitalsComponent); component = fixture.componentInstance; + form = buildForm(); + component.patientVitalsForm = form; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('ngOnInit', () => { + it('initialises language, update button and rbs subscription', () => { + expect(component.currentLanguageSet).toEqual(lang); + expect(component.rbsPopup).toBeFalse(); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + false + ); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + nurse.rbsSelectedInInvestigation$.next(true); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + nurse.rbsSelectedInInvestigation$.next(undefined); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(lang); + }); + }); + + describe('getBeneficiaryDetails', () => { + it('sets female, age and months', () => { + benService.beneficiaryDetails$.next({ + ageVal: 30, + age: '30 years - 4 months', + genderName: 'Female', + }); + expect(component.benAge).toBe(30); + expect(component.totalMonths).toBe(364); + expect(component.female).toBeTrue(); + expect(component.male).toBeFalse(); + }); + + it('sets male flag', () => { + benService.beneficiaryDetails$.next({ + ageVal: 40, + age: '40 years - 0 months', + genderName: 'Male', + }); + expect(component.male).toBeTrue(); + }); + + it('ignores beneficiary without age and gender', () => { + benService.beneficiaryDetails$.next({ ageVal: 0, genderName: null }); + expect(component.benAge).toBe(0); + expect(component.female).toBeFalse(); + expect(component.male).toBeFalse(); + }); + }); + + describe('ngOnChanges', () => { + it('resets doctor fetch and loads vitals in view mode', () => { + spyOn(component, 'getCancerVitals'); + nurse.rbsTestResultFromDoctorFetch = 5; + component.mode = 'view'; + component.ngOnChanges({}); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + expect(component.getCancerVitals).toHaveBeenCalledWith('101', '7'); + }); + + it('updates vitals in update mode', () => { + spyOn(component, 'updateCancerVitals'); + component.mode = 'update'; + component.ngOnChanges({}); + expect(component.updateCancerVitals).toHaveBeenCalled(); + }); + + it('fetches previous anthropometry for nurse only', () => { + spyOn(component, 'getPreviousVisitAnthropometry'); + component.ngOnChanges({}); + expect(component.getPreviousVisitAnthropometry).not.toHaveBeenCalled(); + route.snapshot.params.attendant = 'nurse'; + component.ngOnChanges({}); + expect(component.attendant).toBe('nurse'); + expect(component.getPreviousVisitAnthropometry).toHaveBeenCalled(); + }); + }); + + describe('getPreviousVisitAnthropometry', () => { + it('rounds heights ending with .0', () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: '150.0' } }) + ); + component.getPreviousVisitAnthropometry(); + expect(doctor.getPreviousVisitAnthropometry).toHaveBeenCalledWith({ + benRegID: '101', + }); + expect(form.controls['height_cm'].value).toBe(150); + }); + + it('keeps fractional heights', () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: 150.5 } }) + ); + component.getPreviousVisitAnthropometry(); + expect(form.controls['height_cm'].value).toBe(150.5); + }); + + ['Visit code is not found', 'No data found', null].forEach(resp => { + it(`ignores response ${resp}`, () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: resp } }) + ); + component.getPreviousVisitAnthropometry(); + expect(form.controls['height_cm'].value).toBeNull(); + }); + }); + }); + + describe('RBS', () => { + it('checkDiasableRBS', () => { + expect(component.checkDiasableRBS()).toBeFalse(); + nurse.rbsTestResultFromDoctorFetch = undefined; + expect(component.checkDiasableRBS()).toBeFalse(); + nurse.rbsTestResultFromDoctorFetch = 90; + expect(component.checkDiasableRBS()).toBeTrue(); + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsSelectedInInvestigation = true; + expect(component.checkDiasableRBS()).toBeTrue(); + }); + + it('rbsResultChange disables when fetched from doctor', () => { + form.patchValue({ rbsTestResult: 99 }); + nurse.rbsTestResultFromDoctorFetch = 99; + expect(component.rbsResultChange()).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(99); + expect(form.controls['rbsTestResult'].disabled).toBeTrue(); + expect(form.controls['rbsTestRemarks'].disabled).toBeTrue(); + }); + + it('rbsResultChange enables otherwise', () => { + form.controls['rbsTestRemarks'].disable(); + nurse.rbsTestResultFromDoctorFetch = undefined; + expect(component.rbsResultChange()).toBeFalse(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(null); + expect(form.controls['rbsTestRemarks'].enabled).toBeTrue(); + }); + + it('checkForRange', () => { + form.patchValue({ rbsTestResult: -5 }); + component.checkForRange(); + form.patchValue({ rbsTestResult: 2000 }); + component.checkForRange(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + component.rbsPopup = true; + component.checkForRange(); + form.patchValue({ rbsTestResult: 100 }); + component.rbsPopup = false; + component.checkForRange(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + }); + + describe('vitalsRequiredCheck', () => { + it('notifies height and weight when missing', () => { + const req = buildForm(true); + expect(component.vitalsRequiredCheck(req)).toBeFalse(); + expect(confirmation.notify).toHaveBeenCalledWith( + lang.alerts.info.mandatoryFields, + [ + lang.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.height, + lang.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.weight, + ] + ); + }); + + it('passes when valid', () => { + expect(component.vitalsRequiredCheck(form)).toBeTrue(); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + }); + + describe('updateCancerVitals', () => { + it('skips when required check fails', () => { + component.patientVitalsForm = buildForm(true); + component.updateCancerVitals(); + expect(doctor.updateCancerVitalsDetails).not.toHaveBeenCalled(); + }); + + it('alerts success and marks pristine', () => { + form.patchValue({ height_cm: 160 }); + form.markAsDirty(); + doctor.updateCancerVitalsDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.updateCancerVitals(); + expect(doctor.updateCancerVitalsDetails).toHaveBeenCalledWith( + form.value, + form.getRawValue() + ); + expect(confirmation.alert).toHaveBeenCalledWith('Updated', 'success'); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + false + ); + expect(form.pristine).toBeTrue(); + }); + + it('alerts error on non-200 and on http error', () => { + doctor.updateCancerVitalsDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.updateCancerVitals(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + doctor.updateCancerVitalsDetails.and.returnValue(throwingObs('boom')); + component.updateCancerVitals(); + expect(confirmation.alert).toHaveBeenCalledWith('boom', 'error'); + }); + }); + + describe('ngOnDestroy', () => { + it('unsubscribes and resets doctor fetch', () => { + const ben = jasmine.createSpyObj('s', ['unsubscribe']); + const rbs = jasmine.createSpyObj('s', ['unsubscribe']); + const prev = jasmine.createSpyObj('s', ['unsubscribe']); + const cancer = jasmine.createSpyObj('s', ['unsubscribe']); + component.beneficiaryDetailSubscription = ben; + component.rbsSelectedInInvestigationSubscription = rbs; + component.previousAnthropometryDataSubscription = prev; + component.cancerVitalsSubscription = cancer; + nurse.rbsTestResultFromDoctorFetch = 3; + component.ngOnDestroy(); + // Current behaviour: the cancer vitals branch unsubscribes the + // beneficiary subscription a second time instead of its own. + expect(ben.unsubscribe).toHaveBeenCalledTimes(2); + expect(cancer.unsubscribe).not.toHaveBeenCalled(); + expect(rbs.unsubscribe).toHaveBeenCalled(); + expect(prev.unsubscribe).toHaveBeenCalled(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + }); + + it('tolerates missing subscriptions', () => { + component.beneficiaryDetailSubscription = null; + component.rbsSelectedInInvestigationSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('getCancerVitals', () => { + it('patches form and evaluates derived flags', () => { + spyOn(component, 'canShowBP3').and.callThrough(); + doctor.getCancerVitalsDetails.and.returnValue( + of({ + statusCode: 200, + data: { + benVitalDetails: { + height_cm: 160, + weight_Kg: 60, + waistCircumference_cm: 95, + hbA1C: 6.5, + bloodGlucose_Random: 200, + bloodGlucose_2HrPostPrandial: 200, + bloodGlucose_Fasting: 130, + rbsTestResult: 180, + }, + }, + }) + ); + component.getCancerVitals('101', '7'); + expect(doctor.getCancerVitalsDetails).toHaveBeenCalledWith('101', '7'); + expect(form.controls['height_cm'].value).toBe(160); + expect(component.normalWaist).toBeFalse(); + expect(component.normalHbA1C).toBeFalse(); + expect(component.diabeticRangeRandom).toBeTrue(); + expect(component.diabeticRangePostPrandial).toBeTrue(); + expect(component.diabeticRangeFasting).toBeTrue(); + expect(component.BMI).toBe(23.4); + expect(component.canShowBP3).toHaveBeenCalled(); + expect(nurse.rbsTestResultFromDoctorFetch).toBe(180); + expect(form.controls['rbsTestResult'].disabled).toBeTrue(); + }); + + it('does not mark rbs fetched when result null', () => { + doctor.getCancerVitalsDetails.and.returnValue( + of({ + statusCode: 200, + data: { benVitalDetails: { rbsTestResult: null } }, + }) + ); + component.getCancerVitals('101', '7'); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + expect(form.controls['rbsTestResult'].enabled).toBeTrue(); + }); + + it('ignores missing details, failure and null response', () => { + spyOn(component, 'calculateBMI'); + doctor.getCancerVitalsDetails.and.returnValue( + of({ statusCode: 200, data: { benVitalDetails: null } }) + ); + component.getCancerVitals('1', '2'); + doctor.getCancerVitalsDetails.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getCancerVitals('1', '2'); + doctor.getCancerVitalsDetails.and.returnValue(of(null)); + component.getCancerVitals('1', '2'); + expect(component.calculateBMI).not.toHaveBeenCalled(); + }); + }); + + describe('BMI', () => { + it('computes normal adult BMI', () => { + form.patchValue({ height_cm: 170, weight_Kg: 65 }); + component.calculateBMI(170, 65); + expect(component.BMI).toBe(22.5); + expect(component.normalBMI).toBeTrue(); + expect(nurse.calculateBmiStatus).not.toHaveBeenCalled(); + }); + + it('sets BMI null and abnormal when data missing', () => { + spyOn(component, 'calculateBMIStatusBasedOnAge'); + component.calculateBMI(null, null); + expect(component.BMI).toBeNull(); + expect(component.normalBMI).toBeFalse(); + expect(component.calculateBMIStatusBasedOnAge).not.toHaveBeenCalled(); + }); + + it('uses bmi status service for minors', () => { + component.benGenderAndAge = { + age: '12 years - 0 months', + genderName: 'Female', + }; + component.BMI = 18; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: { bmiStatus: 'NORMAL' } }) + ); + component.calculateBMIStatusBasedOnAge(); + expect(component.totalMonths).toBe(144); + expect(nurse.calculateBmiStatus).toHaveBeenCalledWith({ + yearMonth: '12 years - 0 months', + gender: 'Female', + bmi: 18, + }); + expect(component.bmiStatusMinor).toBe('normal'); + expect(component.normalBMI).toBeTrue(); + }); + + it('minor abnormal / missing status / errors', () => { + component.benGenderAndAge = { + age: '12 years - 0 months', + genderName: 'Male', + }; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: { bmiStatus: 'Obese' } }) + ); + component.calculateBMIStatusBasedOnAge(); + expect(component.normalBMI).toBeFalse(); + + component.normalBMI = true; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + component.calculateBMIStatusBasedOnAge(); + expect(component.normalBMI).toBeTrue(); + + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 500, errorMessage: 'e1' }) + ); + component.calculateBMIStatusBasedOnAge(); + expect(confirmation.alert).toHaveBeenCalledWith('e1', 'error'); + + nurse.calculateBmiStatus.and.returnValue(throwingObs('e2')); + component.calculateBMIStatusBasedOnAge(); + expect(confirmation.alert).toHaveBeenCalledWith('e2', 'error'); + }); + + it('falls back to adult range for adults', () => { + component.benGenderAndAge = { + age: '30 years - 0 months', + genderName: 'Male', + }; + component.BMI = 30; + component.calculateBMIStatusBasedOnAge(); + expect(component.normalBMI).toBeFalse(); + component.BMI = 20; + component.calculateBMIStatusBasedOnAge(); + expect(component.normalBMI).toBeTrue(); + }); + + it('checkHeight / checkWeight recalc BMI and alert out of range', () => { + spyOn(component, 'calculateBMI'); + component.checkHeight(150); + component.checkWeight(60); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkHeight(5); + component.checkWeight(200); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + expect(component.calculateBMI).toHaveBeenCalledTimes(4); + }); + }); + + describe('waist', () => { + it('checkWaist alerts out of range', () => { + component.checkWaist(100); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkWaist(40); + component.checkWaist(160); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + + it('checkNormalWaist for non-pregnant female', () => { + component.female = true; + component.pregnancyStatus = 'No'; + component.checkNormalWaist(75); + expect(component.normalWaist).toBeTrue(); + component.checkNormalWaist(85); + expect(component.normalWaist).toBeFalse(); + }); + + it('checkNormalWaist leaves pregnant female unchanged', () => { + component.female = true; + component.pregnancyStatus = 'Yes'; + component.normalWaist = true; + component.checkNormalWaist(120); + expect(component.normalWaist).toBeTrue(); + }); + + it('checkNormalWaist for male', () => { + component.female = false; + component.checkNormalWaist(85); + expect(component.normalWaist).toBeTrue(); + component.checkNormalWaist(95); + expect(component.normalWaist).toBeFalse(); + }); + }); + + describe('BP', () => { + it('canShowBP3 on diastolic or systolic spread', () => { + form.patchValue({ + diastolicBP_1stReading: 80, + diastolicBP_2ndReading: 90, + }); + component.canShowBP3(); + expect(component.showBP3).toBeTrue(); + form.patchValue({ + diastolicBP_1stReading: 80, + diastolicBP_2ndReading: 82, + systolicBP_1stReading: 120, + systolicBP_2ndReading: 135, + }); + component.canShowBP3(); + expect(component.showBP3).toBeTrue(); + form.patchValue({ systolicBP_2ndReading: 122 }); + component.canShowBP3(); + expect(component.showBP3).toBeFalse(); + }); + + const systolic = [ + 'checkSystolicBP1', + 'checkSystolicBP2', + 'checkSystolicBP3', + ]; + const diastolic = [ + 'checkDiastolicBP1', + 'checkDiastolicBP2', + 'checkDiastolicBP3', + ]; + systolic.forEach(m => { + it(`${m} alerts outside 90-170`, () => { + (component as any)[m](120); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[m](80); + (component as any)[m](180); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + expect(confirmation.alert).toHaveBeenCalledWith(recheck); + }); + }); + diastolic.forEach(m => { + it(`${m} alerts outside 50-110`, () => { + (component as any)[m](80); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[m](40); + (component as any)[m](120); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + }); + + it('BP1/BP2 checks re-evaluate BP3 visibility', () => { + spyOn(component, 'canShowBP3'); + component.checkSystolicBP1(120); + component.checkDiastolicBP1(80); + component.checkSystolicBP2(120); + component.checkDiastolicBP2(80); + expect(component.canShowBP3).toHaveBeenCalledTimes(4); + }); + + [1, 2, 3].forEach(n => { + const suffix = ['1st', '2nd', '3rd'][n - 1]; + it(`checkSystolicGreater${n} clears systolic ${suffix} reading`, () => { + const ctrl = `systolicBP_${suffix}Reading`; + form.patchValue({ [ctrl]: 70 }); + (component as any)[`checkSystolicGreater${n}`](120, 80); + (component as any)[`checkSystolicGreater${n}`](null, 80); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[`checkSystolicGreater${n}`]('70', '80'); + expect(confirmation.alert).toHaveBeenCalledWith(lang.alerts.info.sysBp); + expect(form.controls[ctrl].value).toBeNull(); + }); + + it(`checkDiastolicLesser${n} clears diastolic ${suffix} reading`, () => { + const ctrl = `diastolicBP_${suffix}Reading`; + form.patchValue({ [ctrl]: 130 }); + (component as any)[`checkDiastolicLesser${n}`](120, 80); + (component as any)[`checkDiastolicLesser${n}`](120, null); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[`checkDiastolicLesser${n}`]('120', '130'); + // Uses key `diabp`, which does not exist in the language file. + expect(confirmation.alert).toHaveBeenCalledWith(lang.alerts.info.diabp); + expect(form.controls[ctrl].value).toBeNull(); + }); + }); + }); + + describe('lab value checks', () => { + it('checkHbA1C / checkNormalHbA1c', () => { + component.checkHbA1C(5); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkHbA1C(3); + component.checkHbA1C(7); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + component.checkNormalHbA1c(5.7); + expect(component.normalHbA1C).toBeTrue(); + component.checkNormalHbA1c(5.8); + expect(component.normalHbA1C).toBeFalse(); + }); + + it('checkHemoglobin', () => { + component.checkHemoglobin(12); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkHemoglobin(1); + component.checkHemoglobin(20); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + + it('checkSpo2', () => { + form.patchValue({ sPO2: 98 }); + component.checkSpo2(); + expect(confirmation.alert).not.toHaveBeenCalled(); + form.patchValue({ sPO2: 0 }); + component.checkSpo2(); + form.patchValue({ sPO2: 101 }); + component.checkSpo2(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + + [ + 'checkBloodSugarFasting', + 'checkBloodSugarRandom', + 'checkBloodSugar2HrPostPrandial', + ].forEach(m => { + it(`${m} alerts outside 50-700`, () => { + (component as any)[m](100); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[m](40); + (component as any)[m](800); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + }); + + it('diabetic range flags', () => { + component.checkNormalBloodSugarFasting(100); + expect(component.diabeticRangeFasting).toBeFalse(); + component.checkNormalBloodSugarFasting(101); + expect(component.diabeticRangeFasting).toBeTrue(); + component.checkNormalBloodSugarRandom(140); + expect(component.diabeticRangeRandom).toBeFalse(); + component.checkNormalBloodSugarRandom(141); + expect(component.diabeticRangeRandom).toBeTrue(); + component.checkNormalBloodSugarPostPrandial(140); + expect(component.diabeticRangePostPrandial).toBeFalse(); + component.checkNormalBloodSugarPostPrandial(141); + expect(component.diabeticRangePostPrandial).toBeTrue(); + }); + }); + + describe('getters', () => { + it('expose form values', () => { + form.patchValue({ + waistCircumference_cm: 1, + systolicBP_3rdReading: 2, + diastolicBP_3rdReading: 3, + hbA1C: 4, + hemoglobin: 5, + bloodGlucose_Fasting: 6, + bloodGlucose_Random: 7, + bloodGlucose_2HrPostPrandial: 8, + }); + expect(component.waistCircumference_cm).toBe(1); + expect(component.systolicBP_3rdReading).toBe(2); + expect(component.diastolicBP_3rdReading).toBe(3); + expect(component.hbA1C).toBe(4); + expect(component.hemoglobin).toBe(5); + expect(component.bloodGlucose_Fasting).toBe(6); + expect(component.bloodGlucose_Random).toBe(7); + expect(component.bloodGlucose_2HrPostPrandial).toBe(8); + }); + }); + + describe('IoT dialogs', () => { + const last = () => dialog.open.calls.mostRecent(); + + it('rbs dialog patches result and notifies nurse', () => { + dialogReturns({ result: 140 }); + component.openIOTRBSModel(); + expect(last().args[0]).toBe(IotcomponentComponent); + expect(last().args[1].data.startAPI).toBe(environment.startRBSurl); + expect(component.rbsPopup).toBeFalse(); + expect(form.controls['rbsTestResult'].value).toBe(140); + expect(form.controls['rbsTestResult'].dirty).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(140); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + true + ); + }); + + it('rbs dialog with empty result and null close', () => { + dialogReturns({ result: null }); + component.openIOTRBSModel(); + expect(nurse.setRbsInCurrentVitals).not.toHaveBeenCalled(); + doctor.setValueToEnableVitalsUpdateButton.calls.reset(); + dialogReturns(null); + component.openIOTRBSModel(); + expect(doctor.setValueToEnableVitalsUpdateButton).not.toHaveBeenCalled(); + }); + + it('spo2 and post prandial dialogs, including null close', () => { + dialogReturns({ spo2: 96, result: 150 }); + component.openIOTSPO2Model(); + component.openIOTBGPostPrandialModel(); + expect(form.controls['sPO2'].value).toBe(96); + expect(form.controls['bloodGlucose_2HrPostPrandial'].value).toBe(150); + dialogReturns(null); + component.openIOTSPO2Model(); + component.openIOTBGPostPrandialModel(); + expect(form.controls['sPO2'].value).toBe(96); + expect(form.controls['bloodGlucose_2HrPostPrandial'].value).toBe(150); + }); + + it('weight dialog patches weight and recalculates BMI', () => { + spyOn(component, 'calculateBMI'); + dialogReturns({ result: 55 }); + component.openIOTWeightModel(); + expect(last().args[1].data.startAPI).toBe(environment.startWeighturl); + expect(form.controls['weight_Kg'].value).toBe(55); + expect(component.calculateBMI).toHaveBeenCalledWith(55, 55); + }); + + it('temperature and pulse dialogs', () => { + dialogReturns({ temperature: 99, pulseRate: 70 }); + component.openIOTTempModel(); + component.openIOTPulseRateModel(); + expect(form.controls['temperature'].value).toBe(99); + expect(form.controls['pulseRate'].value).toBe(70); + }); + + it('BP dialogs patch the matching readings', () => { + dialogReturns({ sys: 130, dia: 85 }); + component.openIOTBP1Model(); + component.openIOTBP2Model(); + component.openIOTBP3Model(); + ['1st', '2nd', '3rd'].forEach(s => { + expect(form.controls[`systolicBP_${s}Reading`].value).toBe(130); + expect(form.controls[`diastolicBP_${s}Reading`].value).toBe(85); + }); + expect(last().args[1].data.startAPI).toBe(environment.startBPurl); + }); + + it('hemoglobin and glucose dialogs', () => { + dialogReturns({ result: 11 }); + component.openIOTHemoglobinModel(); + expect(last().args[1].data.startAPI).toBe(environment.startHemoglobinurl); + component.openIOTBGFastingModel(); + component.openIOTBGRandomModel(); + expect(form.controls['hemoglobin'].value).toBe(11); + expect(form.controls['bloodGlucose_Fasting'].value).toBe(11); + expect(form.controls['bloodGlucose_Random'].value).toBe(11); + }); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('Weight'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Weight', + 'Cancer Patient Vitals' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/vitals/general-patient-vitals/general-patient-vitals.component.spec.ts b/src/app/app-modules/nurse-doctor/vitals/general-patient-vitals/general-patient-vitals.component.spec.ts index 736d863c..a108b037 100644 --- a/src/app/app-modules/nurse-doctor/vitals/general-patient-vitals/general-patient-vitals.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/vitals/general-patient-vitals/general-patient-vitals.component.spec.ts @@ -20,2101 +20,1253 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { - async, - ComponentFixture, - tick, - inject, - fakeAsync, - TestBed, -} from '@angular/core/testing'; -import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MaterialModule } from '../../../core/material.module'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, of, Subject } from 'rxjs'; import { GeneralPatientVitalsComponent } from './general-patient-vitals.component'; -import { GeneralUtils } from '../../shared/utility'; - import { ConfirmationService } from '../../../core/services/confirmation.service'; import { BeneficiaryDetailsService } from '../../../core/services/beneficiary-details.service'; -import { NurseService, DoctorService } from '../../shared/services'; - -import { DoctorServiceStub } from '../../shared/mocks/doctor-service-stub'; -import { NurseServiceStub } from '../../shared/mocks/nurse-service-stub'; -import { BeneficiaryDetailsServiceStub } from '../../../core/mocks/beneficiary-details-service-stub'; - -import * as data from '../../shared/mocks/mock-data'; -import { Observable } from 'rxjs/Observable'; - -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { DoctorService } from '../../shared/services/doctor.service'; +import { NurseService } from '../../shared/services/nurse.service'; +import { TestInVitalsService } from '../../shared/services/test-in-vitals.service'; +import { AudioRecordingService } from '../../shared/services/audio-recording.service'; +import { IdrsscoreService } from '../../shared/services/idrsscore.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { IotcomponentComponent } from 'src/app/app-modules/core/components/iotcomponent/iotcomponent.component'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { environment } from 'src/environments/environment'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; + +const CONTROLS = [ + 'height_cm', + 'weight_Kg', + 'waistCircumference_cm', + 'hipCircumference_cm', + 'midUpperArmCircumference_MUAC_cm', + 'headCircumference_cm', + 'temperature', + 'pulseRate', + 'systolicBP_1stReading', + 'diastolicBP_1stReading', + 'respiratoryRate', + 'bMI', + 'bloodGlucose_Fasting', + 'bloodGlucose_Random', + 'bloodGlucose_2hr_PP', + 'sPO2', + 'rbsTestResult', + 'rbsTestRemarks', + 'rbsCheckBox', + 'waistHipRatio', +]; + +function buildForm(required = false): FormGroup { + const group: Record = {}; + CONTROLS.forEach( + c => (group[c] = new FormControl(null, required ? Validators.required : [])) + ); + return new FormGroup(group); +} describe('GeneralPatientVitalsComponent', () => { let component: GeneralPatientVitalsComponent; let fixture: ComponentFixture; - let beneficiaryDetailsService: any; - let nurseService: NurseService; - let doctorService: DoctorService; - let confirmationService: ConfirmationService; - let fb; - let debugElement: DebugElement; - let el: HTMLElement; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ + let form: FormGroup; + let doctor: any; + let nurse: any; + let idrs: any; + let benService: any; + let audio: any; + let testInVitals: any; + let confirmation: any; + let dialog: any; + let session: any; + let tracking: any; + let route: any; + let recordingFailed$: Subject; + let recordedTime$: Subject; + let recordedBlob$: Subject; + const lang = LANGUAGE_EN; + + function dialogReturns(result: any) { + dialog.open.and.returnValue({ afterClosed: () => of(result) }); + } + + beforeEach(async () => { + recordingFailed$ = new Subject(); + recordedTime$ = new Subject(); + recordedBlob$ = new Subject(); + + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [GeneralPatientVitalsComponent], - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], providers: [ - ConfirmationService, + ...commonTestProviders({ + session: { beneficiaryRegID: '101', visitID: '7', visitCode: 'VC1' }, + }), + { + provide: DoctorService, + useValue: autoSpy(DoctorService), + }, + { + provide: NurseService, + useValue: autoSpy(NurseService, { + ncdTemp$: new BehaviorSubject(undefined), + rbsSelectedInInvestigation$: new BehaviorSubject(undefined), + enableLAssessment$: new BehaviorSubject(false), + isAssessmentDone: false, + rbsTestResultFromDoctorFetch: null, + }), + }, + { + provide: IdrsscoreService, + useValue: autoSpy(IdrsscoreService, { + diabetesSelectedFlag$: new BehaviorSubject(0), + }), + }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryDetailsServiceStub, + useValue: autoSpy(BeneficiaryDetailsService, { + beneficiaryDetails$: new BehaviorSubject(null), + }), + }, + { + provide: AudioRecordingService, + useValue: autoSpy(AudioRecordingService, { + recordingFailed: () => recordingFailed$.asObservable(), + getRecordedTime: () => recordedTime$.asObservable(), + getRecordedBlob: () => recordedBlob$.asObservable(), + }), + }, + { + provide: TestInVitalsService, + useValue: autoSpy(TestInVitalsService), + }, + { provide: SetLanguageComponent, useValue: {} }, + { + provide: ActivatedRoute, + useValue: { snapshot: { params: { attendant: 'doctor' } } }, }, - { provide: DoctorService, useClass: DoctorServiceStub }, ], - }).compileComponents(); - })); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(GeneralPatientVitalsComponent, '') + .compileComponents(); + + doctor = TestBed.inject(DoctorService); + nurse = TestBed.inject(NurseService); + idrs = TestBed.inject(IdrsscoreService); + benService = TestBed.inject(BeneficiaryDetailsService); + audio = TestBed.inject(AudioRecordingService); + testInVitals = TestBed.inject(TestInVitalsService); + confirmation = TestBed.inject(ConfirmationService); + dialog = TestBed.inject(MatDialog); + session = TestBed.inject(SessionStorageService); + tracking = TestBed.inject(AmritTrackingService); + route = TestBed.inject(ActivatedRoute); - beforeEach(() => { fixture = TestBed.createComponent(GeneralPatientVitalsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; - - fb = debugElement.injector.get(FormBuilder); - // beneficiaryDetailsService = TestBed.get(BeneficiaryDetailsService); - // doctorService = TestBed.get(DoctorService); - // confirmationService = TestBed.get(ConfirmationService); - component.patientVitalsForm = new GeneralUtils( - fb - ).createGeneralVitalDetailsForm(); - + form = buildForm(); + component.patientVitalsForm = form; + component.visitCategory = 'General OPD'; fixture.detectChanges(); }); - it('should create Vitals Component', () => { - expect(component).toBeTruthy(); + afterEach(() => { + (environment as any).isMMUOfflineSync = false; }); - it('should initialize Vital Component', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); - }); + describe('constructor subscriptions (audio)', () => { + it('resets isRecording when recording fails', () => { + component.isRecording = true; + recordingFailed$.next(true); + expect(component.isRecording).toBeFalse(); + }); - it('should get beneficiary details', () => { - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); + it('stores recorded time and auto-stops at 00:16', () => { + component.isRecording = true; + recordedTime$.next('00:05'); + expect(component.recordedTime).toBe('00:05'); + expect(audio.stopRecording).not.toHaveBeenCalled(); + recordedTime$.next('00:16'); + expect(audio.stopRecording).toHaveBeenCalled(); + expect(component.isRecording).toBeFalse(); + }); + + it('stores recorded blob and builds a blob url', () => { + const blob = new Blob(['abc']); + spyOn(URL, 'createObjectURL').and.returnValue('blob:url'); + recordedBlob$.next({ blob, title: 't' }); + expect(component.coughBlobFile).toBe(blob); + expect(component.blobUrl).toBe('blob:url'); + expect(component.teste.title).toBe('t'); + }); }); - it('should call getBeneficiaryDetails on Initialisation', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.getBeneficiaryDetails).toHaveBeenCalled(); - } - )); + describe('ngOnInit', () => { + it('initialises state and clears service flags', () => { + expect(component.currentLanguageSet).toEqual(lang); + expect(nurse.clearRbsSelectedInInvestigation).toHaveBeenCalled(); + expect(idrs.clearDiabetesSelected).toHaveBeenCalled(); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + false + ); + expect(nurse.clearMessage).toHaveBeenCalled(); + expect(nurse.clearEnableLAssessment).toHaveBeenCalled(); + expect(idrs.clearMessage).toHaveBeenCalled(); + expect(component.ncdTemperature).toBeFalse(); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + expect(component.diabetesSelected).toBe(0); + expect(component.hideLungAssessment).toBeFalse(); + expect(component.enableLungAssessment).toBeFalse(); + }); - it('should get age of a beneficiary', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - } - )); + it('reacts to service stream emissions', () => { + nurse.ncdTemp$.next(true); + expect(component.ncdTemperature).toBeTrue(); + nurse.ncdTemp$.next(undefined); + expect(component.ncdTemperature).toBeFalse(); + nurse.rbsSelectedInInvestigation$.next(true); + expect(component.rbsSelectedInInvestigation).toBeTrue(); + nurse.rbsSelectedInInvestigation$.next(undefined); + expect(component.rbsSelectedInInvestigation).toBeFalse(); + idrs.diabetesSelectedFlag$.next(1); + expect(component.diabetesSelected).toBe(1); + nurse.enableLAssessment$.next(true); + expect(component.enableLungAssessment).toBeTrue(); + nurse.enableLAssessment$.next(false); + expect(component.enableLungAssessment).toBeFalse(); + }); - it('should get check female gender', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); + it('hides lung assessment in offline-sync mode', () => { + (environment as any).isMMUOfflineSync = true; component.ngOnInit(); - expect(component.female).toEqual(true); - } - )); + expect(component.hideLungAssessment).toBeTrue(); + }); - it('should get check female gender', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); + it('disables lung assessment for minors and enables for adults', () => { + benService.beneficiaryDetails$.next({ + ageVal: 10, + age: '10 years - 2 months', + genderName: 'Male', + }); component.ngOnInit(); - expect(component.female).not.toEqual(true); - } - )); + expect(component.disabledLungAssesment).toBeTrue(); + benService.beneficiaryDetails$.next({ + ageVal: 30, + age: '30 years - 0 months', + genderName: 'Male', + }); + component.ngOnInit(); + expect(component.disabledLungAssesment).toBeFalse(); + }); + }); + + describe('getBeneficiaryDetails', () => { + it('sets age, months and female flag', () => { + benService.beneficiaryDetails$.next({ + ageVal: 25, + age: '25 years - 3 months', + genderName: 'Female', + }); + expect(component.benAge).toBe(25); + expect(component.totalMonths).toBe(303); + expect(component.female).toBeTrue(); + expect(component.male).toBeFalse(); + expect(component.disabledLungAssesment).toBeFalse(); + }); + + it('sets male flag and marks minors', () => { + benService.beneficiaryDetails$.next({ + ageVal: 5, + age: '5 years - 1 months', + genderName: 'MALE', + }); + expect(component.male).toBeTrue(); + expect(component.disabledLungAssesment).toBeTrue(); + expect(component.totalMonths).toBe(61); + }); - it('should excute on changes', () => { - component.ngOnChanges(); - expect(component).toBeTruthy(); + it('ignores beneficiary without age or gender', () => { + benService.beneficiaryDetails$.next({ ageVal: null, genderName: null }); + expect(component.benAge).toBeUndefined(); + expect(component.female).toBeUndefined(); + expect(component.male).toBeFalse(); + }); }); - it('should check for visitCategory General OPD and hideForANCAndQC to true and showGlucoseQC to false', () => { - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); + describe('getGender', () => { + [ + ['Female', 1], + ['Male', 0], + ['Transgender', 2], + ].forEach(([g, v]) => { + it(`maps ${g} to ${v}`, () => { + session.store.set('beneficiaryGender', g); + component.getGender(); + expect(component.benGenderType).toBe(v); + }); + }); + + it('leaves gender type unset for unknown gender', () => { + component.benGenderType = undefined; + session.store.set('beneficiaryGender', 'Other'); + component.getGender(); + expect(component.benGenderType).toBeUndefined(); + }); }); - it('should not show mid upper arm circumference', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); + describe('ngOnChanges', () => { + it('flags ANC category', () => { + component.visitCategory = 'ANC'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.hideForANCAndQC).toBeFalse(); + expect(component.showGlucoseQC).toBeFalse(); + expect(component.doctorScreen).toBeFalse(); + }); - it('should not show head circumference', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); + it('flags General OPD (QC) category', () => { + component.visitCategory = 'General OPD (QC)'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.hideForANCAndQC).toBeFalse(); + expect(component.showGlucoseQC).toBeTrue(); + }); - it('should show head circumference', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); + it('defaults for other categories', () => { + component.visitCategory = 'NCD screening'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') - ); - expect(debugElement).toBeTruthy(); - }) - )); + expect(component.hideForANCAndQC).toBeTrue(); + expect(component.showGlucoseQC).toBeFalse(); + }); - it('should show mid upper arm circumference', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); + it('loads vitals and assessment in view mode', () => { + spyOn(component, 'getAssessmentID'); + spyOn(component, 'getGeneralVitalsData'); + component.mode = 'view'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - expect(debugElement).toBeTruthy(); - }) - )); + expect(component.doctorScreen).toBeTrue(); + expect(component.getAssessmentID).toHaveBeenCalled(); + expect(component.getGeneralVitalsData).toHaveBeenCalled(); + }); - it('should not show mid upper arm circumference for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); + it('updates vitals in update mode', () => { + spyOn(component, 'updateGeneralVitals'); + component.mode = 'update'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.doctorScreen).toBeTrue(); + expect(component.updateGeneralVitals).toHaveBeenCalledWith(form); + }); - it('should not show head circumference for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); + it('fetches previous anthropometry for nurse attendant', () => { + spyOn(component, 'getPreviousVisitAnthropometry'); + route.snapshot.params.attendant = 'nurse'; component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.attendant).toBe('nurse'); + expect(component.getPreviousVisitAnthropometry).toHaveBeenCalled(); + }); - it('should not show waistCircumference_cm for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); + it('does not fetch previous anthropometry for doctor', () => { + spyOn(component, 'getPreviousVisitAnthropometry'); component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#waistCircumference_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.getPreviousVisitAnthropometry).not.toHaveBeenCalled(); + }); + }); - it('should not show hipCircumference_cm for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + describe('getPreviousVisitAnthropometry', () => { + it('rounds heights ending in .0', () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: '160.0' } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#hipCircumference_cm')); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.getPreviousVisitAnthropometry(); + expect(doctor.getPreviousVisitAnthropometry).toHaveBeenCalledWith({ + benRegID: '101', + }); + expect(form.controls['height_cm'].value).toBe(160); + }); - it('should not show waistHipRatio for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('keeps fractional heights', () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: 155.5 } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#waistHipRatio')); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.getPreviousVisitAnthropometry(); + expect(form.controls['height_cm'].value).toBe(155.5); + }); + + ['Visit code is not found', 'No data found'].forEach(msg => { + it(`ignores "${msg}"`, () => { + doctor.getPreviousVisitAnthropometry.and.returnValue( + of({ data: { response: msg } }) + ); + component.getPreviousVisitAnthropometry(); + expect(form.controls['height_cm'].value).toBeNull(); + }); + }); + + it('ignores empty response', () => { + doctor.getPreviousVisitAnthropometry.and.returnValue(of(null)); + component.getPreviousVisitAnthropometry(); + expect(form.controls['height_cm'].value).toBeNull(); + }); + }); - it('should not show mid upper arm circumference for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + describe('checkNurseRequirements', () => { + beforeEach(() => { + form = buildForm(true); + component.patientVitalsForm = form; + }); + + it('lists all NCD screening fields', () => { + component.visitCategory = 'NCD screening'; + expect(component.checkNurseRequirements(form)).toBe(0); + const d = lang.vitalsDetails; + expect(confirmation.notify).toHaveBeenCalledWith( + lang.alerts.info.mandatoryFields, + [ + d.AnthropometryDataANC_OPD_NCD_PNC.height, + d.AnthropometryDataANC_OPD_NCD_PNC.weight, + d.vitalsCancerscreening_QC.waistCircumference, + d.vitalsDataANC_OPD_NCD_PNC.temperature, + d.vitalsDataANC_OPD_NCD_PNC.pulseRate, + d.vitalsDataANC_OPD_NCD_PNC.systolicBP, + d.vitalsDataANC_OPD_NCD_PNC.diastolicBP, + lang.rbsTestResult, + ] ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') + }); + + it('lists ANC fields including BP', () => { + component.visitCategory = 'ANC'; + expect(component.checkNurseRequirements(form)).toBe(0); + const args = confirmation.notify.calls.mostRecent().args[1]; + expect(args.length).toBe(6); + expect(args[0]).toBe( + lang.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.systolicBP ); - expect(debugElement).not.toBeTruthy(); - }) - )); + }); + + it('lists general fields for other categories', () => { + component.visitCategory = 'General OPD'; + expect(component.checkNurseRequirements(form)).toBe(0); + expect(confirmation.notify.calls.mostRecent().args[1].length).toBe(4); + }); - it('should not show head circumference for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('requires lung assessment when enabled for adults', () => { + component.patientVitalsForm = buildForm(); + component.hideLungAssessment = true; + component.enableLungAssessment = true; + component.benAge = 20; + nurse.isAssessmentDone = false; + expect(component.checkNurseRequirements(null)).toBe(0); + expect(confirmation.notify.calls.mostRecent().args[1]).toEqual([ + 'Please perform Lung Assessment', + ]); + }); + + it('returns 1 when nothing missing', () => { + component.patientVitalsForm = buildForm(); + component.visitCategory = 'NCD screening'; + expect(component.checkNurseRequirements(null)).toBe(1); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('returns 1 when nothing missing for ANC', () => { + component.patientVitalsForm = buildForm(); + component.visitCategory = 'ANC'; + expect(component.checkNurseRequirements(null)).toBe(1); + }); + }); + + describe('updateGeneralVitals', () => { + it('does nothing when requirements fail', () => { + spyOn(component, 'checkNurseRequirements').and.returnValue(0); + component.updateGeneralVitals(form); + expect(doctor.updateGeneralVitals).not.toHaveBeenCalled(); + }); + + it('alerts success, fetches HRP for ANC and reports RBS', () => { + component.visitCategory = 'ANC'; + spyOn(component, 'getHRPDetails'); + spyOn(component, 'setRBSResultInReport'); + doctor.updateGeneralVitals.and.returnValue( + of({ statusCode: 200, data: { response: 'Saved' } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') + form.markAsDirty(); + component.updateGeneralVitals(form); + expect(doctor.updateGeneralVitals).toHaveBeenCalledWith(form, 'ANC'); + expect(component.getHRPDetails).toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith('Saved', 'success'); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + false ); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.setRBSResultInReport).toHaveBeenCalledWith(form); + expect(form.pristine).toBeTrue(); + }); - it('should not show waistCircumference_cm for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('does not fetch HRP for non-ANC', () => { + spyOn(component, 'getHRPDetails'); + doctor.updateGeneralVitals.and.returnValue( + of({ statusCode: 200, data: { response: 'Saved' } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#waistCircumference_cm') - ); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.updateGeneralVitals(form); + expect(component.getHRPDetails).not.toHaveBeenCalled(); + }); - it('should not show hipCircumference_cm for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('alerts error message on non-200', () => { + doctor.updateGeneralVitals.and.returnValue( + of({ statusCode: 5000, errorMessage: 'bad' }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#hipCircumference_cm')); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.updateGeneralVitals(form); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + }); + + it('alerts on http error', () => { + doctor.updateGeneralVitals.and.returnValue(throwingObs('oops')); + component.updateGeneralVitals(form); + expect(confirmation.alert).toHaveBeenCalledWith('oops', 'error'); + }); + }); + + describe('setRBSResultInReport', () => { + it('sends raw value when rbs result is dirty', () => { + form.patchValue({ rbsTestResult: 120 }); + form.controls['rbsTestResult'].markAsDirty(); + component.setRBSResultInReport(form); + const arg = + testInVitals.setVitalsRBSValueInReportsInUpdate.calls.mostRecent() + .args[0]; + expect(arg.rbsTestResult).toBe(120); + expect(arg.createdDate instanceof Date).toBeTrue(); + }); - it('should not show waistHipRatio for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('sends when remarks dirty', () => { + form.controls['rbsTestRemarks'].markAsDirty(); + component.setRBSResultInReport(form); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).toHaveBeenCalled(); + }); + + it('skips when rbs is disabled or pristine', () => { + component.setRBSResultInReport(form); + form.controls['rbsTestResult'].markAsDirty(); + form.controls['rbsTestResult'].disable(); + component.setRBSResultInReport(form); + component.setRBSResultInReport({ value: null }); + expect( + testInVitals.setVitalsRBSValueInReportsInUpdate + ).not.toHaveBeenCalled(); + }); + }); + + describe('getGeneralVitalsData', () => { + it('patches form and triggers dependent calculations', () => { + component.male = true; + doctor.getGenericVitals.and.returnValue( + of({ + data: { + benAnthropometryDetail: { + height_cm: 170, + weight_Kg: 70, + waistCircumference_cm: 95, + hipCircumference_cm: 100, + waistHipRatio: 0.95, + }, + benPhysicalVitalDetail: { + systolicBP_1stReading: 120, + diastolicBP_1stReading: 80, + rbsTestResult: 110, + }, + }, + benPhysicalVitalDetail: { rbsTestResult: 110 }, + }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#waistHipRatio')); - expect(debugElement).not.toBeTruthy(); - }) - )); + spyOn(component, 'checkHip').and.callThrough(); + spyOn(component, 'hipWaistRatio').and.callThrough(); + component.getGeneralVitalsData(); + expect(doctor.getGenericVitals).toHaveBeenCalledWith({ + benRegID: '101', + benVisitID: '7', + }); + expect(idrs.setSystolicBp).toHaveBeenCalledWith(120); + expect(idrs.setDiastolicBp).toHaveBeenCalledWith(80); + expect(idrs.setIDRSScoreWaist).toHaveBeenCalledWith(10); + expect(nurse.rbsTestResultFromDoctorFetch).toBe(110); + expect(form.controls['rbsTestResult'].disabled).toBeTrue(); + expect(component.checkHip).toHaveBeenCalledWith(100); + expect(component.hipWaistRatio).toHaveBeenCalled(); + expect(form.controls['bMI'].value).toBe(24.2); + expect(testInVitals.setVitalsRBSValueInReports).toHaveBeenCalledWith({ + rbsTestResult: 110, + }); + }); - it('should show bloodGlucose_Fasting for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('handles null readings without side effects', () => { + component.visitCategory = 'ANC'; + doctor.getGenericVitals.and.returnValue( + of({ + data: { + benAnthropometryDetail: { waistCircumference_cm: null }, + benPhysicalVitalDetail: { + systolicBP_1stReading: null, + diastolicBP_1stReading: null, + rbsTestResult: null, + }, + }, + }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Fasting') + component.getGeneralVitalsData(); + expect(idrs.setSystolicBp).not.toHaveBeenCalled(); + expect(idrs.setDiastolicBp).not.toHaveBeenCalled(); + expect(idrs.setIDRSScoreWaist).not.toHaveBeenCalled(); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + expect(testInVitals.setVitalsRBSValueInReports).not.toHaveBeenCalled(); + }); + + it('ignores empty response', () => { + doctor.getGenericVitals.and.returnValue(of(null)); + component.getGeneralVitalsData(); + expect(form.controls['bMI'].value).toBeNull(); + }); + }); + + describe('ngOnDestroy', () => { + it('unsubscribes and resets nurse flags', () => { + const subs = ['a', 'b', 'c', 'd'].map(() => + jasmine.createSpyObj('sub', ['unsubscribe']) ); - expect(debugElement).toBeTruthy(); - }) - )); + component.beneficiaryDetailSubscription = subs[0]; + component.generalVitalsDataSubscription = subs[1]; + component.rbsSelectedInInvestigationSubscription = subs[2]; + component.previousAnthropometryDataSubscription = subs[3]; + nurse.rbsTestResultFromDoctorFetch = 5; + nurse.isAssessmentDone = true; + component.ngOnDestroy(); + subs.forEach(s => expect(s.unsubscribe).toHaveBeenCalled()); + expect(nurse.rbsTestResultFromDoctorFetch).toBeNull(); + expect(nurse.isAssessmentDone).toBeFalse(); + }); + + it('tolerates missing subscriptions', () => { + component.beneficiaryDetailSubscription = null; + component.rbsSelectedInInvestigationSubscription = null; + expect(() => component.ngOnDestroy()).not.toThrow(); + }); + }); + + describe('RBS enable/disable', () => { + it('checkDiasableRBS reflects investigation and doctor fetch', () => { + component.rbsSelectedInInvestigation = false; + nurse.rbsTestResultFromDoctorFetch = null; + expect(component.checkDiasableRBS()).toBeFalse(); + nurse.rbsTestResultFromDoctorFetch = undefined; + expect(component.checkDiasableRBS()).toBeFalse(); + nurse.rbsTestResultFromDoctorFetch = 100; + expect(component.checkDiasableRBS()).toBeTrue(); + nurse.rbsTestResultFromDoctorFetch = null; + component.rbsSelectedInInvestigation = true; + expect(component.checkDiasableRBS()).toBeTrue(); + }); + + it('rbsResultChange disables controls when rbs selected', () => { + form.patchValue({ rbsTestResult: 150 }); + component.rbsSelectedInInvestigation = true; + expect(component.rbsResultChange()).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(150); + expect(form.controls['rbsTestResult'].disabled).toBeTrue(); + expect(form.controls['rbsTestRemarks'].disabled).toBeTrue(); + expect(form.controls['rbsCheckBox'].disabled).toBeTrue(); + }); - it('should show bloodGlucose_Random for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('rbsResultChange enables controls otherwise', () => { + form.controls['rbsTestResult'].disable(); + nurse.rbsTestResultFromDoctorFetch = undefined; + expect(component.rbsResultChange()).toBeFalse(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(null); + expect(form.controls['rbsTestResult'].enabled).toBeTrue(); + }); + + it('onRbsCheckBox toggles flag', () => { + component.onRbsCheckBox({ checked: false }); + expect(component.rbsCheckBox).toBeFalse(); + component.onRbsCheckBox({ checked: true }); + expect(component.rbsCheckBox).toBeTrue(); + }); + + it('checkForRange alerts outside range unless popup open', () => { + form.patchValue({ rbsTestResult: -1 }); + component.checkForRange(); + form.patchValue({ rbsTestResult: 1001 }); + component.checkForRange(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + component.rbsPopup = true; + component.checkForRange(); + form.patchValue({ rbsTestResult: 200 }); + component.rbsPopup = false; + component.checkForRange(); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + }); + + describe('calculateBMI', () => { + it('computes adult BMI and flags normal', () => { + form.patchValue({ height_cm: 170, weight_Kg: 65 }); + component.calculateBMI(); + expect(component.BMI).toBe(22.5); + expect(form.controls['bMI'].value).toBe(22.5); + expect(component.normalBMI).toBeTrue(); + expect(nurse.calculateBmiStatus).not.toHaveBeenCalled(); + }); + + it('clears BMI when height or weight missing', () => { + form.patchValue({ height_cm: 170, bMI: 20 }); + component.BMI = undefined; + component.calculateBMI(); + expect(form.controls['bMI'].value).toBeNull(); + expect(component.normalBMI).toBeFalse(); + }); + + it('uses bmi status service for minors', () => { + component.benGenderAndAge = { + age: '10 years - 2 months', + genderName: 'Male', + }; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: { bmiStatus: 'Normal' } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_Random')); - expect(debugElement).toBeTruthy(); - }) - )); + form.patchValue({ height_cm: 140, weight_Kg: 20 }); + component.calculateBMI(); + expect(component.totalMonths).toBe(122); + expect(nurse.calculateBmiStatus).toHaveBeenCalledWith({ + yearMonth: '10 years - 2 months', + gender: 'Male', + bmi: 10.2, + }); + expect(component.bmiStatusMinor).toBe('normal'); + expect(component.normalBMI).toBeTrue(); + }); - it('should show bloodGlucose_2hr_PP for GOPD (QC)', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('flags abnormal minor bmi status', () => { + component.benGenderAndAge = { + age: '10 years - 2 months', + genderName: 'Female', + }; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: { bmiStatus: 'Underweight' } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_2hr_PP')); - expect(debugElement).toBeTruthy(); - }) - )); + component.calculateBMI(); + expect(component.normalBMI).toBeFalse(); + }); - it('should show bloodGlucose_Fasting for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('ignores missing bmi status', () => { + component.benGenderAndAge = { + age: '10 years - 2 months', + genderName: 'Male', + }; + component.normalBMI = true; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 200, data: { bmiStatus: null } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Fasting') + component.calculateBMI(); + expect(component.normalBMI).toBeTrue(); + }); + + it('alerts on non-200 and on error', () => { + component.benGenderAndAge = { + age: '10 years - 2 months', + genderName: 'Male', + }; + nurse.calculateBmiStatus.and.returnValue( + of({ statusCode: 500, errorMessage: 'fail' }) ); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.calculateBMI(); + expect(confirmation.alert).toHaveBeenCalledWith('fail', 'error'); + nurse.calculateBmiStatus.and.returnValue(throwingObs('err')); + component.calculateBMI(); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + }); + }); + + describe('range checks', () => { + const recheck = () => lang.alerts.info.recheckValue; + + const cases: Array<[string, string, any, any]> = [ + ['checkHeight', 'height_cm', 5, 150], + ['checkWeight', 'weight_Kg', 200, 60], + ['checkHeadCircumference', 'headCircumference_cm', 20, 40], + [ + 'checkMidUpperArmCircumference', + 'midUpperArmCircumference_MUAC_cm', + 5, + 20, + ], + ['checkTemperature', 'temperature', 120, 98], + ['checkPulseRate', 'pulseRate', 40, 80], + ['checkSpo2', 'sPO2', 101, 98], + ['checkRespiratoryRate', 'respiratoryRate', 5, 20], + ]; + cases.forEach(([method, ctrl, bad, good]) => { + it(`${method} alerts only for out-of-range values`, () => { + form.patchValue({ [ctrl]: good }); + (component as any)[method](good); + expect(confirmation.alert).not.toHaveBeenCalled(); + form.patchValue({ [ctrl]: bad }); + (component as any)[method](bad); + expect(confirmation.alert).toHaveBeenCalledWith(recheck()); + }); + }); + + [ + 'checkBloodSugarFasting', + 'checkBloodSugarRandom', + 'checkBloodSugar2HrPostPrandial', + ].forEach(method => { + it(`${method} alerts only for out-of-range values`, () => { + (component as any)[method](100); + expect(confirmation.alert).not.toHaveBeenCalled(); + (component as any)[method](40); + (component as any)[method](800); + expect(confirmation.alert).toHaveBeenCalledTimes(2); + }); + }); + + it('checkSystolic alerts and updates idrs', () => { + component.checkSystolic(120); + expect(confirmation.alert).not.toHaveBeenCalled(); + expect(idrs.setSystolicBp).toHaveBeenCalledWith(120); + component.checkSystolic(30); + expect(confirmation.alert).toHaveBeenCalledWith(recheck()); + component.checkSystolic(null); + expect(idrs.setSystolicBp).toHaveBeenCalledWith(0); + }); - it('should show bloodGlucose_Random for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('checkDiastolic alerts and updates idrs', () => { + component.checkDiastolic(80); + expect(confirmation.alert).not.toHaveBeenCalled(); + expect(idrs.setDiastolicBp).toHaveBeenCalledWith(80); + component.checkDiastolic(200); + expect(confirmation.alert).toHaveBeenCalledWith(recheck()); + component.checkDiastolic(null); + expect(idrs.setDiastolicBp).toHaveBeenCalledWith(0); + }); + + it('checkSystolicGreater clears systolic when not greater', () => { + form.patchValue({ systolicBP_1stReading: 80 }); + component.checkSystolicGreater(120, 80); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkSystolicGreater('80', '90'); + expect(confirmation.alert).toHaveBeenCalledWith(lang.alerts.info.sysBp); + expect(form.controls['systolicBP_1stReading'].value).toBeNull(); + }); + + it('checkDiastolicLower clears diastolic when not lower', () => { + form.patchValue({ diastolicBP_1stReading: 130 }); + component.checkDiastolicLower(120, 80); + component.checkDiastolicLower(null, 80); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.checkDiastolicLower('120', '130'); + expect(confirmation.alert).toHaveBeenCalledWith(lang.alerts.info.diaBp); + expect(form.controls['diastolicBP_1stReading'].value).toBeNull(); + }); + }); + + describe('waist / hip', () => { + it('hipWaistRatio computes ratio with gender thresholds', () => { + form.patchValue({ waistCircumference_cm: 80, hipCircumference_cm: 100 }); + component.female = true; + component.hipWaistRatio(); + expect(component.waistHipRatio).toBe('0.80'); + expect(component.normalWaistHipRatio).toBeTrue(); + component.female = false; + form.patchValue({ waistCircumference_cm: 95 }); + component.hipWaistRatio(); + expect(component.normalWaistHipRatio).toBeFalse(); + }); + + it('hipWaistRatio clears ratio when data missing', () => { + form.patchValue({ waistHipRatio: 1 }); + component.hipWaistRatio(); + expect(component.waistHipRatio).toBeNull(); + }); + + it('checkHip uses gender ranges', () => { + form.patchValue({ hipCircumference_cm: 100 }); + component.female = true; + component.checkHip(100); + expect(component.normalHip).toBeTrue(); + form.patchValue({ hipCircumference_cm: 110 }); + component.checkHip(110); + expect(component.normalHip).toBeFalse(); + component.female = false; + form.patchValue({ hipCircumference_cm: 100 }); + component.checkHip(100); + expect(component.normalHip).toBeTrue(); + form.patchValue({ hipCircumference_cm: 90 }); + component.checkHip(90); + expect(component.normalHip).toBeFalse(); + }); + + const waistCases: Array<[string, number, number]> = [ + ['male', 85, 0], + ['male', 95, 10], + ['male', 105, 20], + ['female', 75, 0], + ['female', 85, 10], + ['female', 95, 20], + ]; + waistCases.forEach(([g, waist, score]) => { + it(`checkIDRSForWaist ${g} ${waist} -> ${score}`, () => { + component.male = g === 'male'; + component.female = g === 'female'; + component.checkIDRSForWaist(waist); + expect(component.IDRSWaistScore).toBe(score); + expect(session.setItem).toHaveBeenCalledWith('waistIDRSScore', score); + expect(idrs.setIDRSScoreWaist).toHaveBeenCalledWith(score); + expect(idrs.setIDRSScoreFlag).toHaveBeenCalled(); + }); + + it(`patchIDRSForWaist ${g} ${waist} -> ${score}`, () => { + component.male = g === 'male'; + component.female = g === 'female'; + component.patchIDRSForWaist(waist); + expect(component.IDRSWaistScore).toBe(score); + expect(idrs.setIDRSScoreWaist).toHaveBeenCalledWith(score); + expect(idrs.setIDRSScoreFlag).not.toHaveBeenCalled(); + }); + }); + + it('waist score untouched when gender unknown', () => { + component.male = false; + component.female = false; + component.checkIDRSForWaist(100); + component.patchIDRSForWaist(100); + expect(component.IDRSWaistScore).toBeUndefined(); + }); + }); + + describe('getters', () => { + it('expose form values', () => { + form.patchValue({ + systolicBP_1stReading: 1, + diastolicBP_1stReading: 2, + bMI: 3, + bloodGlucose_Fasting: 4, + bloodGlucose_Random: 5, + bloodGlucose_2hr_PP: 6, + rbsTestResult: 7, + }); + expect(component.systolicBP_1stReading).toBe(1); + expect(component.diastolicBP_1stReading).toBe(2); + expect(component.bMI).toBe(3); + expect(component.bloodGlucose_Fasting).toBe(4); + expect(component.bloodGlucose_Random).toBe(5); + expect(component.bloodGlucose_2hr_PP).toBe(6); + expect(component.rbsTestResult).toBe(7); + }); + }); + + describe('IoT dialogs', () => { + function lastDialogConfig() { + return dialog.open.calls.mostRecent(); + } + + it('weight dialog patches weight and recalculates BMI', () => { + spyOn(component, 'calculateBMI'); + dialogReturns({ result: 70 }); + component.openIOTWeightModel(); + expect(lastDialogConfig().args[0]).toBe(IotcomponentComponent); + expect(lastDialogConfig().args[1].data.startAPI).toBe( + environment.startWeighturl ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_Random')); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(form.controls['weight_Kg'].value).toBe(70); + expect(doctor.setValueToEnableVitalsUpdateButton).toHaveBeenCalledWith( + true + ); + expect(component.calculateBMI).toHaveBeenCalled(); + }); - it('should show bloodGlucose_2hr_PP for ANC', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('rbs dialog patches result and notifies nurse service', () => { + dialogReturns({ result: 130 }); + component.openIOTRBSModel(); + expect(component.rbsPopup).toBeFalse(); + expect(form.controls['rbsTestResult'].value).toBe(130); + expect(form.controls['rbsTestResult'].dirty).toBeTrue(); + expect(nurse.setRbsInCurrentVitals).toHaveBeenCalledWith(130); + }); + + it('rbs dialog with empty result skips nurse service', () => { + dialogReturns({ result: null }); + component.openIOTRBSModel(); + expect(nurse.setRbsInCurrentVitals).not.toHaveBeenCalled(); + }); + + it('rbs dialog closed with null does nothing', () => { + dialogReturns(null); + component.openIOTRBSModel(); + expect(component.rbsPopup).toBeFalse(); + expect( + doctor.setValueToEnableVitalsUpdateButton + ).not.toHaveBeenCalledWith(true); + }); + + it('spo2 dialog patches sPO2, null ignored', () => { + dialogReturns({ spo2: 97 }); + component.openIOTSPO2Model(); + expect(form.controls['sPO2'].value).toBe(97); + dialogReturns(null); + component.openIOTSPO2Model(); + expect(form.controls['sPO2'].value).toBe(97); + }); + + it('temperature dialog patches temperature', () => { + dialogReturns({ temperature: 98.6 }); + component.openIOTTempModel(); + expect(form.controls['temperature'].value).toBe(98.6); + }); + + it('pulse dialog patches pulse rate', () => { + dialogReturns({ pulseRate: 72 }); + component.openIOTPulseRateModel(); + expect(form.controls['pulseRate'].value).toBe(72); + }); + + it('bp dialog patches both readings', () => { + dialogReturns({ sys: 120, dia: 80 }); + component.openIOTBPModel(); + expect(form.controls['systolicBP_1stReading'].value).toBe(120); + expect(form.controls['diastolicBP_1stReading'].value).toBe(80); + }); + + it('blood glucose dialogs patch their controls', () => { + dialogReturns({ result: 90 }); + component.openIOTBGFastingModel(); + component.openIOTBGRandomModel(); + component.openIOTBGPostPrandialModel(); + expect(form.controls['bloodGlucose_Fasting'].value).toBe(90); + expect(form.controls['bloodGlucose_Random'].value).toBe(90); + expect(form.controls['bloodGlucose_2hr_PP'].value).toBe(90); + expect(lastDialogConfig().args[1].data.startAPI).toBe( + environment.startBloodGlucoseurl ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_2hr_PP')); - expect(debugElement).not.toBeTruthy(); - }) - )); + }); + }); - it('should hide bloodGlucose_Fasting for General OPD', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + describe('getHRPDetails', () => { + it('sets HRP positive', () => { + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: true } }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Fasting') + component.getHRPDetails(); + expect(doctor.getHRPDetails).toHaveBeenCalledWith('101', 'VC1'); + expect(benService.setHRPPositive).toHaveBeenCalled(); + }); + + it('resets HRP when negative', () => { + doctor.getHRPDetails.and.returnValue( + of({ statusCode: 200, data: { isHRP: false } }) ); - expect(debugElement).not.toBeTruthy(); - }) - )); + component.getHRPDetails(); + expect(benService.resetHRPPositive).toHaveBeenCalled(); + }); + + it('ignores empty response', () => { + doctor.getHRPDetails.and.returnValue(of(null)); + component.getHRPDetails(); + expect(benService.setHRPPositive).not.toHaveBeenCalled(); + expect(benService.resetHRPPositive).not.toHaveBeenCalled(); + }); + }); + + describe('language', () => { + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(lang); + }); + }); - it('should show bloodGlucose_Random for General OPD', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + describe('audio recording', () => { + it('start/stop/abort only act on correct state', () => { + component.stopRecording(); + component.abortRecording(); + expect(audio.stopRecording).not.toHaveBeenCalled(); + expect(audio.abortRecording).not.toHaveBeenCalled(); + + component.startRecording(); + component.startRecording(); + expect(audio.startRecording).toHaveBeenCalledTimes(1); + expect(component.isRecording).toBeTrue(); + component.abortRecording(); + expect(audio.abortRecording).toHaveBeenCalled(); + expect(component.isRecording).toBeFalse(); + + component.startRecording(); + component.stopRecording(); + expect(audio.stopRecording).toHaveBeenCalled(); + expect(component.isRecording).toBeFalse(); + }); + + it('clearRecordedData resets state on confirm', () => { + component.blobUrl = 'x'; + component.frequentCough = true; + component.enableResult = true; + nurse.isAssessmentDone = true; + component.clearRecordedData(); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + 'Do you really want to clear the recording?' ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_Random')); - expect(debugElement).not.toBeTruthy(); - }) - )); + expect(component.blobUrl).toBeNull(); + expect(component.frequentCough).toBeFalse(); + expect(component.enableResult).toBeFalse(); + expect(nurse.isAssessmentDone).toBeFalse(); + expect(component.coughBlobFile.size).toBe(0); + }); - it('should show bloodGlucose_2hr_PP for General OPD', async( - inject([BeneficiaryDetailsService], beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 + it('clearRecordedData keeps state on cancel', () => { + confirmation.confirm.and.returnValue(of(false)); + component.blobUrl = 'x'; + component.clearRecordedData(); + expect(component.blobUrl).toBe('x'); + }); + + it('onCheckboxChange does not mutate component symptom flags', () => { + component.onCheckboxChange(component.sputum, { checked: true }); + expect(component.sputum).toBeFalse(); + }); + }); + + describe('startAssessment', () => { + beforeEach(() => { + session.store.set('providerServiceID', '5'); + session.store.set('userName', 'nurse1'); + component.benGenderType = 1; + component.benAge = 30; + component.frequentCough = true; + component.wheezing = true; + }); + + it('posts form data and stores result', () => { + audio.getResultStatus.and.returnValue( + of({ + statusCode: 200, + data: { + severity: 'mild', + cough_pattern: 'dry', + cough_severity_score: 2, + record_duration: 10, + }, + }) ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - debugElement = fixture.debugElement.query(By.css('#bloodGlucose_2hr_PP')); - expect(debugElement).not.toBeTruthy(); - }) - )); - - it('Check normal value of height', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - spyOn(component, 'checkHeight'); - component.patientVitalsForm.patchValue({ height_cm: '50' }); - debugElement = fixture.debugElement.query(By.css('#height_cm')); - debugElement.nativeElement.value = '50'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.height_cm).toEqual('50'); - fixture.detectChanges(); - expect(component.checkHeight).toHaveBeenCalled(); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkHeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).not.toHaveBeenCalled(); - } - ) - )); - - it('Check Abnormal value of height', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - spyOn(component, 'checkHeight').and.callThrough(); - component.patientVitalsForm.patchValue({ height_cm: 210 }); - debugElement = fixture.debugElement.query(By.css('#height_cm')); - debugElement.nativeElement.value = '210'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.height_cm).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkHeight).toHaveBeenCalled(); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkHeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check Abnormal value of weight_Kg', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - spyOn(component, 'checkWeight').and.callThrough(); - component.patientVitalsForm.patchValue({ weight_Kg: '210' }); - debugElement = fixture.debugElement.query(By.css('#weight_Kg')); - debugElement.nativeElement.value = '210'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.weight_Kg).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWeight).toHaveBeenCalled(); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check Normal value of weight_Kg', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - fixture.detectChanges(); - spyOn(component, 'checkWeight').and.callThrough(); - component.patientVitalsForm.patchValue({ weight_Kg: '50' }); - debugElement = fixture.debugElement.query(By.css('#weight_Kg')); - debugElement.nativeElement.value = '50'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.weight_Kg).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkWeight).toHaveBeenCalled(); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check hip of female patient hipCircumference_cm', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkHip').and.callThrough(); - component.patientVitalsForm.patchValue({ hipCircumference_cm: '90' }); - debugElement = fixture.debugElement.query( - By.css('#hipCircumference_cm') - ); - debugElement.nativeElement.value = '90'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.hipCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHip).toHaveBeenCalled(); - // fixture.detectChanges(); - console.log(component.female); - console.log(component.normalHip); - fixture.detectChanges(); - expect(component.female).toEqual(true); - expect(component.normalHip).toEqual(false); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check hip of female patient hipCircumference_cm normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - fixture.detectChanges(); - spyOn(component, 'checkHip').and.callThrough(); - component.patientVitalsForm.patchValue({ hipCircumference_cm: '100' }); - debugElement = fixture.debugElement.query( - By.css('#hipCircumference_cm') - ); - debugElement.nativeElement.value = '100'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.hipCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHip).toHaveBeenCalled(); - //fixture.detectChanges; - expect(component.female).toEqual(true); - console.log(component.female); - console.log(component.normalHip); - expect(component.normalHip).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check hip of male patient hipCircumference_cm normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.maleBeneficiary.ageVal); - expect(component.female).toEqual(false); - //fixture.detectChanges(); - spyOn(component, 'checkHip').and.callThrough(); - component.patientVitalsForm.patchValue({ hipCircumference_cm: '100' }); - debugElement = fixture.debugElement.query( - By.css('#hipCircumference_cm') - ); - debugElement.nativeElement.value = '100'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.hipCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHip).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - console.log(component.normalHip); - expect(component.female).toEqual(false); - expect(component.normalHip).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check hip of male patient hipCircumference_cm abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next(data.maleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.maleBeneficiary.ageVal); - expect(component.female).toEqual(false); - //fixture.detectChanges(); - spyOn(component, 'checkHip').and.callThrough(); - component.patientVitalsForm.patchValue({ hipCircumference_cm: '90' }); - debugElement = fixture.debugElement.query( - By.css('#hipCircumference_cm') - ); - debugElement.nativeElement.value = '90'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.hipCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHip).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - console.log(component.normalHip); - expect(component.female).toEqual(false); - expect(component.normalHip).toEqual(false); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check headCircumference_cm for child Abnormal ', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkHeadCircumference').and.callThrough(); - component.patientVitalsForm.patchValue({ headCircumference_cm: '20' }); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') - ); - debugElement.nativeElement.value = '20'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.headCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHeadCircumference).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check midUpperArmCircumference_MUAC_cm for child Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiaryage_6 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual( - data.femaleChildBeneficiaryage_6.ageVal - ); - expect(component.female).toEqual(true); - //fixture.detectChanges(); - spyOn(component, 'checkMidUpperArmCircumference').and.callThrough(); - component.patientVitalsForm.patchValue({ - midUpperArmCircumference_MUAC_cm: '4', - }); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - debugElement.nativeElement.value = '4'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.midUpperArmCircumference_MUAC_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkMidUpperArmCircumference).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check headCircumference_cm for child Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiary_2 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleChildBeneficiary_2.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkHeadCircumference').and.callThrough(); - component.patientVitalsForm.patchValue({ headCircumference_cm: '23' }); - debugElement = fixture.debugElement.query( - By.css('#headCircumference_cm') - ); - debugElement.nativeElement.value = '23'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.headCircumference_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkHeadCircumference).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check midUpperArmCircumference_MUAC_cm for child Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiaryage_6 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual( - data.femaleChildBeneficiaryage_6.ageVal - ); - expect(component.female).toEqual(true); - //fixture.detectChanges(); - spyOn(component, 'checkMidUpperArmCircumference').and.callThrough(); - component.patientVitalsForm.patchValue({ - midUpperArmCircumference_MUAC_cm: '7', - }); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - //fixture.detectChanges(); - expect(component.midUpperArmCircumference_MUAC_cm).toEqual( - debugElement.nativeElement.value - ); - //fixture.detectChanges(); - expect(component.checkMidUpperArmCircumference).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check midUpperArmCircumference_MUAC_cm for child Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiaryage_6 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual( - data.femaleChildBeneficiaryage_6.ageVal - ); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkMidUpperArmCircumference').and.callThrough(); - component.patientVitalsForm.patchValue({ - midUpperArmCircumference_MUAC_cm: '7', - }); - debugElement = fixture.debugElement.query( - By.css('#midUpperArmCircumference_MUAC_cm') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.midUpperArmCircumference_MUAC_cm).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkMidUpperArmCircumference).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check temperature for child Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiaryage_6 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual( - data.femaleChildBeneficiaryage_6.ageVal - ); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkTemperature').and.callThrough(); - component.patientVitalsForm.patchValue({ temperature: '7' }); - debugElement = fixture.debugElement.query(By.css('#temperature')); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.temperature).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkTemperature).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check temperature for child Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleChildBeneficiaryage_6 - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual( - data.femaleChildBeneficiaryage_6.ageVal - ); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkTemperature').and.callThrough(); - component.patientVitalsForm.patchValue({ temperature: '97' }); - debugElement = fixture.debugElement.query(By.css('#temperature')); - debugElement.nativeElement.value = '97'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.temperature).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkTemperature).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check pulseRate Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkPulseRate').and.callThrough(); - component.patientVitalsForm.patchValue({ pulseRate: '7' }); - debugElement = fixture.debugElement.query(By.css('#pulseRate')); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.pulseRate).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkPulseRate).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check pulseRate Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkPulseRate').and.callThrough(); - component.patientVitalsForm.patchValue({ pulseRate: '97' }); - debugElement = fixture.debugElement.query(By.css('#pulseRate')); - debugElement.nativeElement.value = '97'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.pulseRate).toEqual(debugElement.nativeElement.value); - fixture.detectChanges(); - expect(component.checkPulseRate).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check respiratoryRate Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkRespiratoryRate').and.callThrough(); - component.patientVitalsForm.patchValue({ respiratoryRate: '97' }); - debugElement = fixture.debugElement.query(By.css('#respiratoryRate')); - debugElement.nativeElement.value = '97'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.respiratoryRate).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkRespiratoryRate).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check respiratoryRate Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkRespiratoryRate').and.callThrough(); - component.patientVitalsForm.patchValue({ respiratoryRate: '7' }); - debugElement = fixture.debugElement.query(By.css('#respiratoryRate')); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.respiratoryRate).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkRespiratoryRate).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check systolicBP_1stReading Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkSystolic').and.callThrough(); - component.patientVitalsForm.patchValue({ systolicBP_1stReading: '7' }); - debugElement = fixture.debugElement.query( - By.css('#systolicBP_1stReading') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.systolicBP_1stReading).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkSystolic).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check systolicBP_1stReading normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkSystolic').and.callThrough(); - component.patientVitalsForm.patchValue({ systolicBP_1stReading: '97' }); - debugElement = fixture.debugElement.query( - By.css('#systolicBP_1stReading') - ); - debugElement.nativeElement.value = '97'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.systolicBP_1stReading).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkSystolic).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check diastolicBP_1stReading normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkDiastolic').and.callThrough(); - component.patientVitalsForm.patchValue({ - diastolicBP_1stReading: '97', - }); - debugElement = fixture.debugElement.query( - By.css('#diastolicBP_1stReading') - ); - debugElement.nativeElement.value = '97'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.diastolicBP_1stReading).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkDiastolic).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check diastolicBP_1stReading Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(true); - expect(component.showGlucoseQC).toEqual(false); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkDiastolic').and.callThrough(); - component.patientVitalsForm.patchValue({ diastolicBP_1stReading: '7' }); - debugElement = fixture.debugElement.query( - By.css('#diastolicBP_1stReading') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.diastolicBP_1stReading).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkDiastolic).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Fasting Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugarFasting').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_Fasting: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Fasting') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_Fasting).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugarFasting).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Random Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugarRandom').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_Random: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Random') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_Random).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugarRandom).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Random Abnormal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugar2HrPostPrandial').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_2hr_PP: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_2hr_PP') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_2hr_PP).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugar2HrPostPrandial).toHaveBeenCalled(); - fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Fasting Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugarFasting').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_Fasting: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Fasting') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_Fasting).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugarFasting).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Random Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugarRandom').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_Random: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_Random') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_Random).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugarRandom).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('Check bloodGlucose_Random Normal', async( - inject( - [BeneficiaryDetailsService, ConfirmationService], - (beneficiaryDetailsService, confirmationService) => { - beneficiaryDetailsService.beneficiaryDetails.next( - data.femaleBeneficiary - ); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); - expect(component.benAge).toEqual(data.femaleBeneficiary.ageVal); - expect(component.female).toEqual(true); - fixture.detectChanges(); - spyOn(component, 'checkBloodSugar2HrPostPrandial').and.callThrough(); - component.patientVitalsForm.patchValue({ bloodGlucose_2hr_PP: '7' }); - debugElement = fixture.debugElement.query( - By.css('#bloodGlucose_2hr_PP') - ); - debugElement.nativeElement.value = '7'; - const el = debugElement.nativeElement as HTMLElement; - el.dispatchEvent(new Event('input')); - el.dispatchEvent(new Event('change')); - fixture.detectChanges(); - expect(component.bloodGlucose_2hr_PP).toEqual( - debugElement.nativeElement.value - ); - fixture.detectChanges(); - expect(component.checkBloodSugar2HrPostPrandial).toHaveBeenCalled(); - //fixture.detectChanges; - console.log(component.female); - expect(component.female).toEqual(true); - // spyOn(confirmationService, 'alert'); - // spyOn(component, 'checkWeight').and.callThrough(); - // fixture.detectChanges(); - // expect(confirmationService.alert).toHaveBeenCalled(); - } - ) - )); - - it('should get check female gender', inject( - [BeneficiaryDetailsService], - beneficiaryDetailsService => { - beneficiaryDetailsService.beneficiaryDetails.next(data.femaleBeneficiary); - spyOn(component, 'getBeneficiaryDetails'); - component.ngOnInit(); - expect(component.female).toEqual(true); - } - )); + component.startAssessment(); + const fd: FormData = audio.getResultStatus.calls.mostRecent().args[0]; + const req = JSON.parse(fd.get('request') as string); + expect(req.gender).toBe(1); + expect(req.age).toBe(30); + expect(req.patientId).toBe('101'); + expect(req.providerServiceMapID).toBe('5'); + expect(req.createdBy).toBe('nurse1'); + expect(req.symptoms).toEqual({ + frequent_cough: 1, + sputum: 0, + cough_at_night: 0, + wheezing: 1, + pain_in_chest: 0, + shortness_of_breath: 0, + }); + expect((fd.get('file') as File).name).toBe('coughSound.wav'); + expect(component.severity).toBe('mild'); + expect(component.cough_pattern).toBe('dry'); + expect(component.cough_severity_score).toBe(2); + expect(component.record_duration).toBe(10); + expect(nurse.setEnableLAssessment).toHaveBeenCalledWith(false); + expect(component.enableResult).toBeTrue(); + expect(nurse.isAssessmentDone).toBeTrue(); + }); - it('should check for visitCategory General OPD (QC) and hideForANCAndQC to false and showGlucoseQC to ture', () => { - component.visitCategory = new String('General OPD (QC)'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(true); + it('alerts on non-200 and http error', () => { + audio.getResultStatus.and.returnValue( + of({ statusCode: 500, errorMessage: 'no' }) + ); + component.startAssessment(); + expect(confirmation.alert).toHaveBeenCalledWith('no', 'error'); + audio.getResultStatus.and.returnValue(throwingObs('x')); + component.startAssessment(); + expect(confirmation.alert).toHaveBeenCalledWith('x', 'error'); + expect(component.enableResult).toBeFalse(); + }); }); - it('should check for visitCategory ANC and hideForANCAndQC to false and showGlucoseQC false', () => { - component.visitCategory = new String('ANC'); - component.ngOnChanges(); - expect(component.hideForANCAndQC).toEqual(false); - expect(component.showGlucoseQC).toEqual(false); + describe('assessment lookup', () => { + it('getAssessmentID fetches details of the latest assessment', () => { + spyOn(component, 'getAssessmentDetails'); + doctor.getAssessment.and.returnValue( + of({ + statusCode: 200, + data: [{ assessmentId: 1 }, { assessmentId: 9 }], + }) + ); + component.getAssessmentID(); + expect(doctor.getAssessment).toHaveBeenCalledWith('101'); + expect(component.getAssessmentDetails).toHaveBeenCalledWith(9); + }); - it('should get GeneralVitalsData when mode is view', () => { - spyOn(component, 'getGeneralVitalsData'); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); + it('getAssessmentID skips when no id or no data', () => { + spyOn(component, 'getAssessmentDetails'); + doctor.getAssessment.and.returnValue( + of({ statusCode: 200, data: [{ assessmentId: null }] }) + ); + component.getAssessmentID(); + doctor.getAssessment.and.returnValue(of({ statusCode: 200, data: [] })); + component.getAssessmentID(); + expect(component.getAssessmentDetails).not.toHaveBeenCalled(); }); - it('should get GeneralVitalsData when mode is view and patch height_cm', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.height_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.height_cm - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch weight_Kg', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.weight_Kg).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.weight_Kg - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch bMI', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.bMI).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.bMI - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch waistCircumference_cm', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.waistCircumference_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail - .waistCircumference_cm - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch hipCircumference_cm', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.hipCircumference_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail - .hipCircumference_cm - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch waistHipRatio', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(+component.patientVitalsForm.value.waistHipRatio).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.waistHipRatio - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch temperature', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.temperature).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.temperature - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and pulseRate', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.pulseRate).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.pulseRate - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch systolicBP_1stReading', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.systolicBP_1stReading).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail - .systolicBP_1stReading - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch diastolicBP_1stReading', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect( - component.patientVitalsForm.value.diastolicBP_1stReading - ).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail - .diastolicBP_1stReading - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch respiratoryRate', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.respiratoryRate).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.respiratoryRate - ); - }) - )); - - it('should get GeneralVitalsData when mode is view and patch all fields', async( - inject([DoctorService], doctorService => { - localStorage.setItem('visitID', '66'); - localStorage.setItem('beneficiaryRegID', '7416'); - localStorage.setItem('visitCategory', 'General OPD'); - component.visitCategory = 'General OPD'; - spyOn(doctorService, 'getGenericVitals').and.returnValue( - Observable.of(data.generalOPDVitalData.data) - ); - spyOn(component, 'getGeneralVitalsData').and.callThrough(); - component.mode = new String('view'); - component.ngOnChanges(); - expect(component.getGeneralVitalsData).toHaveBeenCalled(); - expect(doctorService.getGenericVitals).toHaveBeenCalled(); - fixture.detectChanges(); - expect(component.patientVitalsForm.value.height_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.height_cm - ); - expect(component.patientVitalsForm.value.weight_Kg).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.weight_Kg - ); - expect(component.patientVitalsForm.value.bMI).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.bMI - ); - expect(component.patientVitalsForm.value.waistCircumference_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail - .waistCircumference_cm - ); - expect(component.patientVitalsForm.value.hipCircumference_cm).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail - .hipCircumference_cm - ); - expect(+component.patientVitalsForm.value.waistHipRatio).toEqual( - data.generalOPDVitalData.data.benAnthropometryDetail.waistHipRatio - ); - expect(component.patientVitalsForm.value.temperature).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.temperature - ); - expect(component.patientVitalsForm.value.pulseRate).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.pulseRate - ); - expect(component.patientVitalsForm.value.systolicBP_1stReading).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail - .systolicBP_1stReading - ); - expect( - component.patientVitalsForm.value.diastolicBP_1stReading - ).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail - .diastolicBP_1stReading - ); - expect(component.patientVitalsForm.value.respiratoryRate).toEqual( - data.generalOPDVitalData.data.benPhysicalVitalDetail.respiratoryRate - ); - }) - )); + it('getAssessmentDetails stores result', () => { + doctor.getAssessmentDet.and.returnValue( + of({ + statusCode: 200, + data: { + severity: 'high', + cough_pattern: 'wet', + cough_severity_score: 8, + record_duration: 12, + }, + }) + ); + component.getAssessmentDetails(9); + expect(doctor.getAssessmentDet).toHaveBeenCalledWith(9); + expect(component.severity).toBe('high'); + expect(component.record_duration).toBe(12); + expect(component.enableResult).toBeTrue(); + expect(nurse.isAssessmentDone).toBeTrue(); + }); + + it('getAssessmentDetails ignores failure', () => { + doctor.getAssessmentDet.and.returnValue( + of({ statusCode: 500, data: null }) + ); + component.getAssessmentDetails(9); + expect(component.enableResult).toBeFalse(); + }); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('Height'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Height', + 'Vitals' + ); }); }); diff --git a/src/app/app-modules/nurse-doctor/vitals/vitals.component.spec.ts b/src/app/app-modules/nurse-doctor/vitals/vitals.component.spec.ts index ef688dfc..90f0d4f3 100644 --- a/src/app/app-modules/nurse-doctor/vitals/vitals.component.spec.ts +++ b/src/app/app-modules/nurse-doctor/vitals/vitals.component.spec.ts @@ -20,115 +20,88 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MaterialModule } from '../../core/material.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; import { VitalsComponent } from './vitals.component'; +import { SetLanguageComponent } from '../../core/components/set-language.component'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + commonTestProviders, +} from 'src/testing/test-utils'; describe('VitalsComponent', () => { let component: VitalsComponent; let fixture: ComponentFixture; - let debugElement; - let fb; + let el: HTMLElement; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [BrowserAnimationsModule, ReactiveFormsModule, MaterialModule], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [VitalsComponent], + providers: [...commonTestProviders()], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(VitalsComponent); component = fixture.componentInstance; - debugElement = fixture.debugElement; + el = fixture.nativeElement; + component.patientVitalsDataForm = new FormGroup({}); + component.visitCategory = ''; + component.vitalsMode = ''; + component.pregnancyStatus = ''; fixture.detectChanges(); }); - it('VitalsComponent should be created', () => { + it('creates with language set and no child rendered', () => { expect(component).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect( + component.languageComponent instanceof SetLanguageComponent + ).toBeTrue(); + expect(component.showGeneralOPD).toBeFalse(); + expect(component.showCancer).toBeFalse(); + expect(el.querySelector('app-nurse-cancer-patient-vitals')).toBeNull(); + expect(el.querySelector('app-nurse-general-patient-vitals')).toBeNull(); }); - it('should be created', () => { + it('ngOnInit and ngDoCheck fetch language', () => { + spyOn(component, 'fetchLanguageResponse'); component.ngOnInit(); - expect(component).toBeTruthy(); - }); - - it('should excute on changes', () => { - component.ngOnChanges(); - expect(component).toBeTruthy(); - }); - - it('Should show general vital while selecting visitCategory other than Cancer Screening', () => { - component.ngOnInit(); - component.visitCategory = 'ANC'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - }); - - it('Should show cancer vital', () => { - component.ngOnInit(); - component.visitCategory = 'Cancer Screening'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); + component.ngDoCheck(); + expect(component.fetchLanguageResponse).toHaveBeenCalledTimes(2); }); - it('Should show cancer vital', () => { - component.ngOnInit(); + it('shows cancer vitals for Cancer Screening', () => { component.visitCategory = 'Cancer Screening'; component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-cancer-patient-vitals') - ); - expect(de.nativeElement).toBeTruthy(); + expect(component.showCancer).toBeTrue(); + expect(component.showGeneralOPD).toBeFalse(); + expect(el.querySelector('app-nurse-cancer-patient-vitals')).not.toBeNull(); + expect(el.querySelector('app-nurse-general-patient-vitals')).toBeNull(); }); - it('Should show cancer vital', () => { - component.ngOnInit(); - component.visitCategory = 'General OPD'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-general-patient-vitals') - ); - expect(de.nativeElement).toBeTruthy(); + ['General OPD', 'ANC', 'PNC', 'NCD screening'].forEach(cat => { + it(`shows general vitals for ${cat}`, () => { + component.visitCategory = cat; + component.ngOnChanges(); + fixture.detectChanges(); + expect(component.showCancer).toBeFalse(); + expect(component.showGeneralOPD).toBeTrue(); + expect( + el.querySelector('app-nurse-general-patient-vitals') + ).not.toBeNull(); + }); }); - it('Should not show general vital', () => { - component.ngOnInit(); - component.visitCategory = 'Cancer Screening'; + it('leaves flags untouched when visitCategory is empty', () => { + component.showCancer = true; + component.visitCategory = ''; component.ngOnChanges(); - expect(component.showCancer).toEqual(true); - expect(component.showGeneralOPD).toEqual(false); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-general-patient-vitals') - ); - expect(de).not.toBeTruthy(); - }); - - it('Should not show cancer vital', () => { - component.ngOnInit(); - component.visitCategory = 'General OPD'; - component.ngOnChanges(); - expect(component.showCancer).toEqual(false); - expect(component.showGeneralOPD).toEqual(true); - fixture.detectChanges(); - const de = fixture.debugElement.query( - By.css('nurse-cancer-patient-vitals') - ); - expect(de).not.toBeTruthy(); + expect(component.showCancer).toBeTrue(); + expect(component.showGeneralOPD).toBeFalse(); }); }); diff --git a/src/app/app-modules/nurse-doctor/workarea/workarea-can-activate.service.spec.ts b/src/app/app-modules/nurse-doctor/workarea/workarea-can-activate.service.spec.ts new file mode 100644 index 00000000..a857c7d0 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/workarea/workarea-can-activate.service.spec.ts @@ -0,0 +1,128 @@ +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { WorkareaCanActivate } from './workarea-can-activate.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +describe('WorkareaCanActivate (nurse-doctor)', () => { + let guard: WorkareaCanActivate; + let sessionStorageSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + + beforeEach(() => { + sessionStorageSpy = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + + TestBed.configureTestingModule({ + providers: [ + WorkareaCanActivate, + { provide: Router, useValue: routerSpy }, + { provide: SessionStorageService, useValue: sessionStorageSpy }, + ], + }); + guard = TestBed.inject(WorkareaCanActivate); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + describe('when visitCategory is present', () => { + const visitCategoryBranchKeys = [ + 'visitCode', + 'beneficiaryGender', + 'benFlowID', + 'visitCategory', + 'beneficiaryRegID', + 'visitID', + 'beneficiaryID', + 'nurseFlag', + ]; + + it('should return true when all required keys are present', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + const store: Record = { + visitCode: 'VC001', + beneficiaryGender: 'Male', + benFlowID: '222', + visitCategory: 'General OPD', + beneficiaryRegID: '9876', + visitID: '111', + beneficiaryID: '333', + nurseFlag: '2', + }; + return store[key] || null; + }); + + expect(guard.canActivate(null as any, null as any)).toBeTrue(); + }); + + visitCategoryBranchKeys.forEach(missingKey => { + if (missingKey === 'visitCategory') return; + it(`should return false when '${missingKey}' is missing`, () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === missingKey) return null; + const store: Record = { + visitCode: 'VC001', + beneficiaryGender: 'Male', + benFlowID: '222', + visitCategory: 'General OPD', + beneficiaryRegID: '9876', + visitID: '111', + beneficiaryID: '333', + nurseFlag: '2', + }; + return store[key] || null; + }); + + expect(guard.canActivate(null as any, null as any)).toBeFalse(); + }); + }); + }); + + describe('when visitCategory is absent (falsy)', () => { + const noVisitCategoryKeys = [ + 'beneficiaryGender', + 'beneficiaryRegID', + 'beneficiaryID', + 'benFlowID', + ]; + + it('should return true when all 4 required keys are present', () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + const store: Record = { + beneficiaryGender: 'Female', + beneficiaryRegID: '9876', + beneficiaryID: '333', + benFlowID: '222', + }; + return store[key] || null; + }); + + expect(guard.canActivate(null as any, null as any)).toBeTrue(); + }); + + noVisitCategoryKeys.forEach(missingKey => { + it(`should return false when '${missingKey}' is missing`, () => { + sessionStorageSpy.getItem.and.callFake((key: string) => { + if (key === missingKey) return null; + const store: Record = { + beneficiaryGender: 'Female', + beneficiaryRegID: '9876', + beneficiaryID: '333', + benFlowID: '222', + }; + return store[key] || null; + }); + + expect(guard.canActivate(null as any, null as any)).toBeFalse(); + }); + }); + + it('should return false when all keys are missing', () => { + sessionStorageSpy.getItem.and.returnValue(null); + expect(guard.canActivate(null as any, null as any)).toBeFalse(); + }); + }); +}); diff --git a/src/app/app-modules/nurse-doctor/workarea/workarea.component.spec.ts b/src/app/app-modules/nurse-doctor/workarea/workarea.component.spec.ts new file mode 100644 index 00000000..b72b2f15 --- /dev/null +++ b/src/app/app-modules/nurse-doctor/workarea/workarea.component.spec.ts @@ -0,0 +1,2652 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + AbstractControl, + FormArray, + FormControl, + FormGroup, + Validators, +} from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; + +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; +import { WorkareaComponent } from './workarea.component'; +import { + DoctorService, + MasterdataService, + NurseService, +} from '../shared/services'; +import { IdrsscoreService } from '../shared/services/idrsscore.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { SetLanguageComponent } from '../../core/components/set-language.component'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; +import { SmsNotificationComponent } from '../sms-notification/sms-notification.component'; +import { OpenPreviousVisitDetailsComponent } from '../../core/components/open-previous-visit-details/open-previous-visit-details.component'; + +/* ------------------------------------------------------------------ */ +/* Local form-building helpers */ +/* ------------------------------------------------------------------ */ + +/** Marker: a FormControl that is required and empty (has errors). */ +const REQ = '__REQUIRED__'; +/** Wrap a value so it is kept as a single FormControl (not expanded). */ +class Val { + constructor(public v: any) {} +} +const val = (v: any) => new Val(v); + +/** Recursively build Angular form controls from a plain description. */ +function build(x: any): AbstractControl { + if (x === REQ) return new FormControl(null, Validators.required); + if (x instanceof Val) return new FormControl(x.v); + if (x instanceof AbstractControl) return x; + if (Array.isArray(x)) return new FormArray(x.map(build)); + if (x && typeof x === 'object' && !(x instanceof Date)) { + const g: any = {}; + Object.keys(x).forEach(k => (g[k] = build(x[k]))); + return new FormGroup(g); + } + return new FormControl(x); +} +const e = (err: boolean) => (err ? REQ : 'ok'); + +const L = LANGUAGE_EN; + +function vitals(err = false) { + return { + systolicBP_1stReading: e(err), + diastolicBP_1stReading: e(err), + height_cm: e(err), + weight_Kg: e(err), + temperature: e(err), + pulseRate: e(err), + waistCircumference_cm: e(err), + rbsCheckBox: false, + rbsTestResult: 'ok', + }; +} + +function refer(o: any = {}) { + return { + referredToInstituteName: o.inst ?? null, + referredToInstituteID: o.instID ?? null, + refrredToAdditionalServiceList: val(o.list === undefined ? null : o.list), + referralReason: o.reasonErr ? REQ : 'reason', + referralReasonList: o.reasonListErr ? REQ : 'reason', + }; +} + +/** Build a medical form usable by checkNurseRequirements. */ +function nurseForm(o: any = {}) { + const desc: any = { + patientVisitForm: { + patientCovidForm: { + contactStatus: e(!!o.covidErr), + travelStatus: e(!!o.covidErr), + symptom: e(!!o.covidErr), + }, + }, + patientHistoryForm: { + pastObstericHistory: { pastObstericHistoryList: o.preg ?? [] }, + personalHistory: { allergicList: o.allergies ?? [] }, + comorbidityHistory: { + comorbidityConcurrentConditionsList: [ + { comorbidConditions: e(!!o.comorbidErr) }, + ], + }, + }, + patientReferForm: refer(o.refer), + }; + if (!o.noVitals) desc.patientVitalsForm = vitals(!!o.vitalsErr); + if (o.exam !== undefined) { + desc.patientExaminationForm = { + generalExaminationForm: { + typeOfDangerSigns: e(o.exam), + lymphnodesInvolved: e(o.exam), + typeOfLymphadenopathy: e(o.exam), + extentOfEdema: e(o.exam), + edemaType: e(o.exam), + }, + }; + } + if (o.pncErr !== undefined) { + desc.patientPNCForm = { + deliveryPlace: e(o.pncErr), + deliveryType: e(o.pncErr), + }; + } + if (o.ancErr !== undefined) { + desc.patientANCForm = { + patientANCDetailsForm: { + primiGravida: e(o.ancErr), + lmpDate: e(o.ancErr), + }, + }; + } + if (o.caseRecord) desc.patientCaseRecordForm = o.caseRecord; + return build(desc) as FormGroup; +} + +function caseRecord(o: any = {}) { + return { + generalDoctorInvestigationForm: { + labTest: o.labTest === undefined ? [] : o.labTest, + }, + generalDiagnosisForm: { + provisionalDiagnosisList: [ + { + provisionalDiagnosis: o.provErr ? REQ : (o.prov ?? 'fever'), + conceptID: o.concept === undefined ? '123' : o.concept, + }, + ], + confirmatoryDiagnosisList: [ + { + confirmatoryDiagnosis: o.conf ?? null, + conceptID: o.confConcept === undefined ? null : o.confConcept, + }, + ], + doctorDiagnosis: e(!!o.docDiagErr), + ncdScreeningConditionArray: + o.ncdCond === REQ ? REQ : val(o.ncdCond ?? ['Diabetes']), + ncdScreeningConditionOther: o.ncdOther ?? null, + }, + diagnosisForm: { provisionalDiagnosisPrimaryDoctor: e(!!o.cancerDiagErr) }, + provisionalDiagnosisPrimaryDoctor: e(!!o.cancerDiagErr), + drugPrescriptionForm: { + prescribedDrugs: + o.drugs === undefined ? [{ createdBy: 'doc', drugName: 'x' }] : o.drugs, + }, + }; +} + +describe('WorkareaComponent', () => { + let component: WorkareaComponent; + let fixture: ComponentFixture; + let nurseService: any; + let doctorService: any; + let masterdataService: any; + let idrs: any; + let beneficiarySvc: any; + let confirmation: any; + let session: any; + let router: Router; + let dialog: any; + let routeParams: any; + + const SERVICE_LINE = JSON.stringify({ vanID: 11, parkingPlaceID: 22 }); + + beforeEach(async () => { + routeParams = { attendant: 'nurse' }; + nurseService = autoSpy(NurseService, { + ncdTemp$: new BehaviorSubject(undefined), + enableLAssessment$: new BehaviorSubject(false), + enableProvisionalDiag$: new BehaviorSubject(false), + fileData: undefined, + isAssessmentDone: false, + }); + doctorService = autoSpy(DoctorService, { + enableVitalsUpdateButton$: new BehaviorSubject(undefined), + covidVaccineAgeGroup: null, + enableCovidVaccinationButton: false, + prescribedDrugData: 'drugs', + }); + doctorService.postGeneralRefer.and.returnValue({ refer: true }); + masterdataService = autoSpy(MasterdataService, { + visitDetailMasterData$: new BehaviorSubject(null), + }); + idrs = { + tmcSubmitDisable$: new BehaviorSubject(false), + rBSPresentFlag$: new BehaviorSubject(0), + visualAcuityPresentFlag$: new BehaviorSubject(0), + heamoglobinPresentFlag$: new BehaviorSubject(0), + diabetesSelectedFlag$: new BehaviorSubject(0), + VisualAcuityTestMandatoryFlag$: new BehaviorSubject(0), + }; + beneficiarySvc = { + beneficiaryDetails$: new BehaviorSubject({ ageVal: 35 }), + }; + + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [WorkareaComponent], + providers: [ + ...commonTestProviders({ + session: { + serviceLineDetails: SERVICE_LINE, + providerServiceID: 4, + userName: 'nurse1', + userID: 9, + beneficiaryRegID: 100, + visitID: 200, + visitCode: 300, + serviceID: 2, + benFlowID: 7, + beneficiaryID: 8, + sessionID: 3, + }, + }), + { provide: NurseService, useValue: nurseService }, + { provide: DoctorService, useValue: doctorService }, + { provide: MasterdataService, useValue: masterdataService }, + { provide: IdrsscoreService, useValue: idrs }, + { provide: BeneficiaryDetailsService, useValue: beneficiarySvc }, + { provide: SetLanguageComponent, useValue: {} }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + get params() { + return routeParams; + }, + }, + }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(WorkareaComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(WorkareaComponent); + component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService) as any; + session = TestBed.inject(SessionStorageService) as any; + dialog = TestBed.inject(MatDialog) as any; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + spyOn(console, 'log'); + }); + + afterEach(() => { + fixture.destroy(); + sessionStorage.clear(); + localStorage.clear(); + }); + + const init = ( + sessionValues: Record = {}, + attendant = 'nurse' + ) => { + routeParams.attendant = attendant; + Object.keys(sessionValues).forEach(k => + session.store.set(k, sessionValues[k]) + ); + fixture.detectChanges(); + }; + + /* ================================================================ */ + describe('initialisation', () => { + it('creates and initialises in new-lookup mode when no visit category', () => { + init(); + expect(component).toBeTruthy(); + expect(component.newLookupMode).toBeTrue(); + expect(component.isSpecialist).toBeFalse(); + expect(component.doctorUpdateAndTCSubmit).toBe(L.common.update); + expect(component.beneficiaryRegID).toBe(100); + expect(component.visitID).toBe(200); + expect(nurseService.clearMessage).toHaveBeenCalled(); + expect(masterdataService.getVisitDetailMasterData).toHaveBeenCalled(); + expect(doctorService.checkUsersignatureExist).toHaveBeenCalledWith(9); + expect(component.patientVisitForm).toBeTruthy(); + expect(component.beneficiary).toEqual({ ageVal: 35 }); + expect(component.beneficiaryAge).toBe(35); + }); + + it('sets specialist values for tcspecialist attendant', () => { + init({}, 'tcspecialist'); + expect(component.isSpecialist).toBeTrue(); + expect(component.doctorUpdateAndTCSubmit).toBe(L.common.submit); + }); + + it('stores doctor signature status when API returns data', () => { + doctorService.checkUsersignatureExist.and.returnValue( + of({ statusCode: 200, data: { signStatus: true } }) + ); + init(); + expect(component.doctorSignatureFlag).toBeTrue(); + }); + + it('ignores signature response without data', () => { + doctorService.checkUsersignatureExist.and.returnValue( + of({ statusCode: 200, data: null }) + ); + init(); + expect(component.doctorSignatureFlag).toBeFalse(); + }); + + it('reacts to idrs / nurse / doctor subject emissions', () => { + init(); + idrs.tmcSubmitDisable$.next(true); + idrs.rBSPresentFlag$.next(1); + idrs.visualAcuityPresentFlag$.next(2); + idrs.heamoglobinPresentFlag$.next(3); + idrs.diabetesSelectedFlag$.next(1); + idrs.VisualAcuityTestMandatoryFlag$.next(1); + nurseService.ncdTemp$.next(true); + nurseService.enableLAssessment$.next(true); + nurseService.enableProvisionalDiag$.next(true); + doctorService.enableVitalsUpdateButton$.next(true); + expect(component.tmcDisable).toBeTrue(); + expect(component.rbsPresent).toBe(1); + expect(component.visualAcuityPresent).toBe(2); + expect(component.heamoglobinPresent).toBe(3); + expect(component.diabetesSelected).toBe(1); + expect(component.visualAcuityMandatory).toBe(1); + expect(component.ncdTemperature).toBeTrue(); + expect(component.enableLungAssessment).toBeTrue(); + expect(component.enableProvisionalDiag).toBeTrue(); + expect(component.enableUpdateButtonInVitals).toBeTrue(); + + nurseService.ncdTemp$.next(undefined); + nurseService.enableLAssessment$.next(false); + nurseService.enableProvisionalDiag$.next(false); + doctorService.enableVitalsUpdateButton$.next(undefined); + expect(component.ncdTemperature).toBeFalse(); + expect(component.enableLungAssessment).toBeFalse(); + expect(component.enableProvisionalDiag).toBeFalse(); + expect(component.enableUpdateButtonInVitals).toBeFalse(); + }); + + it('ignores empty beneficiary emissions', () => { + beneficiarySvc.beneficiaryDetails$.next(null); + init(); + expect(component.beneficiary).toBeUndefined(); + }); + + it('opens view mode directly when visit category is in session', () => { + init({ visitCategory: 'General OPD' }, 'doctor'); + expect(component.newLookupMode).toBeFalse(); + expect(component.showCaseRecord).toBeTrue(); + expect(component.visitMode.toString()).toBe('view'); + expect( + component.patientVisitForm.get('patientVisitDetailsForm.visitCategory') + ?.disabled + ).toBeTrue(); + }); + + it('shows only TM referred details when specialist flag is 100', () => { + init({ specialist_flag: '100' }); + expect(component.showTMVisitDetails).toBeTrue(); + expect(component.showVisitDetails).toBeFalse(); + expect(component.showVitals).toBeFalse(); + }); + + it('handles a visit category selected in new-lookup mode', () => { + init(); + masterdataService.visitDetailMasterData$.next({ + visitCategories: [{ visitCategory: 'NCD care', visitCategoryID: 5 }], + }); + component.patientVisitForm + .get('patientVisitDetailsForm.visitCategory') + ?.setValue('NCD care'); + expect(masterdataService.reset).toHaveBeenCalled(); + expect(component.visitCategory).toBe('NCD care'); + expect(masterdataService.getNurseMasterData).toHaveBeenCalledWith(5, 4); + expect(component.showVitals).toBeTrue(); + expect(component.showHistory).toBeTrue(); + expect(component.showCaseRecord).toBeFalse(); + }); + + it('ignores an empty visit category selection', () => { + init(); + component.patientVisitForm + .get('patientVisitDetailsForm.visitCategory') + ?.setValue(null); + expect(masterdataService.reset).not.toHaveBeenCalled(); + }); + + it('loads nurse and doctor master data when category known', () => { + init({ visitCategory: 'ANC' }); + masterdataService.visitDetailMasterData$.next({ + visitCategories: [{ visitCategory: 'ANC', visitCategoryID: 3 }], + }); + expect(component.visitCategoryList.length).toBe(1); + expect(masterdataService.getNurseMasterData).toHaveBeenCalledWith(3, 4); + expect(masterdataService.getDoctorMasterData).toHaveBeenCalledWith(3, 4); + }); + + it('skips master data when category id not found', () => { + init({ visitCategory: 'Unknown' }); + masterdataService.visitDetailMasterData$.next({ + visitCategories: [{ visitCategory: 'ANC', visitCategoryID: 3 }], + }); + expect(masterdataService.getNurseMasterData).not.toHaveBeenCalled(); + expect(masterdataService.getDoctorMasterData).not.toHaveBeenCalled(); + }); + }); + + /* ================================================================ */ + describe('getVisitCategoryID', () => { + beforeEach(() => init()); + + it('returns null without list or category', () => { + component.visitCategoryList = undefined; + expect(component.getVisitCategoryID('ANC')).toBeNull(); + component.visitCategoryList = []; + expect(component.getVisitCategoryID('')).toBeNull(); + }); + + it('returns the matching id', () => { + component.visitCategoryList = [ + { visitCategory: 'PNC', visitCategoryID: 9 }, + ]; + expect(component.getVisitCategoryID('PNC')).toBe(9); + expect(component.getVisitCategoryID('ANC')).toBeNull(); + }); + }); + + /* ================================================================ */ + describe('handleVisitType per visit category', () => { + beforeEach(() => init({}, 'nurse')); + + it('does nothing for a falsy category', () => { + component.showVitals = true; + component.handleVisitType(null); + expect(component.showVitals).toBeTrue(); + }); + + it('General OPD (QC) in view mode builds quick consult & refer forms', () => { + component.handleVisitType('General OPD (QC)', 'view'); + expect(component.showQuickConsult).toBeTrue(); + expect(component.showRefer).toBeTrue(); + expect(component.patientQuickConsultForm).toBeTruthy(); + expect(component.patientReferForm).toBeTruthy(); + expect(component.quickConsultMode.toString()).toBe('view'); + }); + + it('General OPD (QC) in new mode builds vitals only', () => { + component.handleVisitType('General OPD (QC)'); + expect(component.showVitals).toBeTrue(); + expect(component.showQuickConsult).toBeFalse(); + expect(component.patientVitalsForm).toBeTruthy(); + }); + + it('Cancer Screening new mode shows history/vitals/examination', () => { + component.handleVisitType('Cancer Screening'); + expect(component.showHistory).toBeTrue(); + expect(component.showExamination).toBeTrue(); + expect(component.showCaseRecord).toBeFalse(); + component.patientVitalsForm.patchValue({ height_cm: 150 }); + expect(component.currentVitals).toBeTruthy(); + }); + + it('Cancer Screening view mode patches cancer findings', () => { + component.handleVisitType('Cancer Screening', 'view'); + expect(component.showCaseRecord).toBeTrue(); + expect(component.caseRecordMode.toString()).toBe('view'); + ( + component.patientExaminationForm.get('oralExaminationForm') as FormGroup + ).patchValue({ observation: 'oral-obs' }); + expect(component.findings.oralExamination).toBe('oral-obs'); + }); + + it('General OPD view mode patches general findings', () => { + component.handleVisitType('General OPD', 'view'); + expect(component.showCaseRecord).toBeTrue(); + expect(component.examinationMode.toString()).toBe('view'); + const cc = component.patientVisitForm.get( + 'patientChiefComplaintsForm' + ) as FormGroup; + cc.markAsDirty(); + (cc.get('complaints') as FormArray).at(0).patchValue({ + chiefComplaint: 'cough', + }); + expect(component.findings).toBeTruthy(); + }); + + it('General OPD new mode', () => { + component.handleVisitType('General OPD'); + expect(component.showExamination).toBeTrue(); + expect(component.showCaseRecord).toBeFalse(); + }); + + it('NCD screening view and new modes', () => { + component.handleVisitType('NCD screening'); + expect(component.showNCDScreening).toBeTrue(); + expect(component.idrsScreeningForm).toBeTruthy(); + expect(component.showCaseRecord).toBeFalse(); + component.handleVisitType('NCD screening', 'view'); + expect(component.ncdScreeningMode.toString()).toBe('view'); + expect(component.showRefer).toBeTrue(); + }); + + it('PNC view and new modes', () => { + component.handleVisitType('PNC'); + expect(component.showPNC).toBeTrue(); + expect(component.showCaseRecord).toBeFalse(); + component.handleVisitType('PNC', 'view'); + expect(component.pncMode.toString()).toBe('view'); + expect(component.patientCaseRecordForm).toBeTruthy(); + }); + + it('ANC new mode wires LMP / primigravida / gravida patches', () => { + component.handleVisitType('ANC'); + expect(component.showAnc).toBeTrue(); + const details = component.patientANCForm.get( + 'patientANCDetailsForm' + ) as FormGroup; + details.patchValue({ lmpDate: '2024-01-01', primiGravida: true }); + expect(component.primeGravidaStatus).toBeTrue(); + expect( + component.patientHistoryForm.get('menstrualHistory.lMPDate')?.value + ).toEqual(new Date('2024-01-01')); + details.patchValue({ lmpDate: null }); + component.patientANCForm + .get('obstetricFormulaForm.gravida_G') + ?.setValue(3); + expect( + component.patientHistoryForm.get('pastObstericHistory.totalNoOfPreg') + ?.value + ).toBe(3); + component.patientANCForm + .get('obstetricFormulaForm.gravida_G') + ?.setValue(1); + expect( + component.patientHistoryForm.get('pastObstericHistory.totalNoOfPreg') + ?.value + ).toBe(3); + }); + + it('ANC view mode copies obstetric values into diagnosis', () => { + component.handleVisitType('ANC', 'view'); + expect(component.ancMode.toString()).toBe('view'); + const diag = component.patientCaseRecordForm.get( + 'generalDiagnosisForm' + ) as FormGroup; + const patchSpy = spyOn(diag, 'patchValue').and.callThrough(); + component.patientANCForm + .get('obstetricFormulaForm.gravida_G') + ?.setValue(2); + component.patientANCForm + .get('patientANCDetailsForm.primiGravida') + ?.setValue(false); + expect(patchSpy).toHaveBeenCalledTimes(2); + }); + + it('NCD care view mode', () => { + component.handleVisitType('NCD care', 'view'); + expect(component.showCaseRecord).toBeTrue(); + expect(component.referMode.toString()).toBe('view'); + }); + + it('COVID-19 Screening new and view modes', () => { + component.handleVisitType('COVID-19 Screening'); + expect(component.showVitals).toBeTrue(); + expect(component.showCaseRecord).toBeFalse(); + component.handleVisitType('COVID-19 Screening', 'view'); + expect(component.showCaseRecord).toBeTrue(); + expect(component.historyMode.toString()).toBe('view'); + }); + + it('unknown category just hides everything', () => { + component.showVitals = true; + component.handleVisitType('Something else'); + expect(component.showVitals).toBeFalse(); + }); + + it('specialist flag 100 shows TM referred only', () => { + component.specialistFlag = '100'; + component.handleVisitType('ANC'); + expect(component.showTMVisitDetails).toBeTrue(); + }); + + it('hideAll removes forms and triggers change detection for nurse', () => { + component.handleVisitType('General OPD', 'view'); + const cdr = (component as any).changeDetectorRef; + spyOn(cdr, 'detectChanges'); + component.hideAll(); + expect(component.patientMedicalForm.get('patientVitalsForm')).toBeNull(); + expect(cdr.detectChanges).toHaveBeenCalled(); + component.attendantType = 'doctor'; + cdr.detectChanges.calls.reset(); + component.hideAll(); + expect(cdr.detectChanges).not.toHaveBeenCalled(); + }); + }); + + /* ================================================================ */ + describe('submit dispatchers', () => { + beforeEach(() => init()); + + const nurseMap: Array<[string, string]> = [ + ['Cancer Screening', 'submitNurseCancerVisitDetails'], + ['NCD screening', 'submitNurseNCDScreeningVisitDetails'], + ['General OPD (QC)', 'submitNurseQuickConsultVisitDetails'], + ['ANC', 'submitNurseANCVisitDetails'], + ['PNC', 'submitPatientMedicalDetailsPNC'], + ['General OPD', 'submitNurseGeneralOPDVisitDetails'], + ['NCD care', 'submitNurseNCDcareVisitDetails'], + ['COVID-19 Screening', 'submitNurseCovidcareVisitDetails'], + ]; + nurseMap.forEach(([cat, method]) => { + it(`nurse submit routes "${cat}" to ${method}`, () => { + const spies = nurseMap.map(([, m]) => spyOn(component as any, m)); + component.visitCategory = cat; + const form = { f: 1 }; + component.submitPatientMedicalDetailsForm(form); + expect(component.disableSubmitButton).toBeTrue(); + expect(component.showProgressBar).toBeTrue(); + spies.forEach((s, i) => { + if (nurseMap[i][1] === method) { + expect(s).toHaveBeenCalledWith(form); + } else expect(s).not.toHaveBeenCalled(); + }); + }); + }); + + const doctorMap: Array<[string, string]> = [ + ['Cancer Screening', 'submitCancerDiagnosisForm'], + ['General OPD (QC)', 'submitQuickConsultDiagnosisForm'], + ['ANC', 'submitANCDiagnosisForm'], + ['PNC', 'submitPNCDiagnosisForm'], + ['General OPD', 'submitGeneralOPDDiagnosisForm'], + ['NCD care', 'submitNCDCareDiagnosisForm'], + ['COVID-19 Screening', 'submitCovidCareDiagnosisForm'], + ['NCD screening', 'submitNCDScreeningDiagnosisForm'], + ]; + doctorMap.forEach(([cat, method]) => { + it(`doctor submit routes "${cat}" to ${method}`, () => { + const spies = doctorMap.map(([, m]) => spyOn(component as any, m)); + component.visitCategory = cat; + component.submitDoctorDiagnosisForm(); + expect(component.disableSubmitButton).toBeTrue(); + spies.forEach((s, i) => { + if (doctorMap[i][1] === method) expect(s).toHaveBeenCalled(); + else expect(s).not.toHaveBeenCalled(); + }); + }); + }); + }); + + /* ================================================================ */ + describe('nurse submit flows', () => { + const cases: Array<[string, string, string]> = [ + [ + 'submitNurseQuickConsultVisitDetails', + 'postNurseGeneralQCVisitForm', + 'checkNurseRequirements', + ], + [ + 'submitNurseANCVisitDetails', + 'postNurseANCVisitForm', + 'checkNurseRequirements', + ], + [ + 'submitNurseNCDcareVisitDetails', + 'postNurseNCDCareVisitForm', + 'checkNurseRequirements', + ], + [ + 'submitNurseCovidcareVisitDetails', + 'postNurseCovidCareVisitForm', + 'checkNurseRequirements', + ], + [ + 'submitNurseNCDScreeningVisitDetails', + 'postNCDScreeningForm', + 'checkNCDScreeningRequiredData', + ], + [ + 'submitPatientMedicalDetailsPNC', + 'postNursePNCVisitForm', + 'checkNurseRequirements', + ], + [ + 'submitNurseGeneralOPDVisitDetails', + 'postNurseGeneralOPDVisitForm', + 'checkNurseRequirements', + ], + ]; + + beforeEach(() => { + init(); + component.beneficiary = { ageVal: 40 }; + }); + + cases.forEach(([method, svc, check]) => { + describe(method, () => { + it('does not post when validation fails', () => { + spyOn(component as any, check).and.returnValue(0); + (component as any)[method]({}); + expect(nurseService[svc]).not.toHaveBeenCalled(); + }); + + it('navigates to nurse worklist on success', () => { + spyOn(component as any, check).and.returnValue(1); + sessionStorage.setItem('benFlowID', '1'); + nurseService[svc].and.returnValue( + of({ statusCode: 200, data: { response: 'saved' } }) + ); + const resetSpy = spyOn(component.patientMedicalForm, 'reset'); + (component as any)[method]({}); + expect(resetSpy).toHaveBeenCalled(); + expect(sessionStorage.getItem('benFlowID')).toBeNull(); + expect(confirmation.alert).toHaveBeenCalledWith('saved', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + + it('alerts error on non-200 response', () => { + spyOn(component as any, check).and.returnValue(1); + component.disableSubmitButton = true; + nurseService[svc].and.returnValue( + of({ statusCode: 5000, errorMessage: 'bad' }) + ); + (component as any)[method]({}); + expect(component.disableSubmitButton).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts error when request fails', () => { + spyOn(component as any, check).and.returnValue(1); + component.showProgressBar = true; + nurseService[svc].and.returnValue(throwingObs('boom')); + (component as any)[method]({}); + expect(component.showProgressBar).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('boom', 'error'); + }); + }); + }); + }); + + /* ================================================================ */ + describe('nurse cancer submit', () => { + beforeEach(() => { + init(); + spyOn(component, 'getImageCoordinates').and.returnValue([{ x: 1 }]); + }); + + it('does nothing when cancer validation fails', () => { + spyOn(component, 'checkCancerRequiredData').and.returnValue(false); + component.submitNurseCancerVisitDetails({}); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('does not post when confirm returns null', () => { + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + confirmation.confirm.and.returnValue(of(null)); + component.submitNurseCancerVisitDetails({}); + expect(nurseService.postNurseCancerVisitForm).not.toHaveBeenCalled(); + }); + + it('posts and navigates on success', () => { + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + nurseService.postNurseCancerVisitForm.and.returnValue( + of({ statusCode: 200, data: { response: 'ok' } }) + ); + const form = { a: 1 }; + component.submitNurseCancerVisitDetails(form); + expect(nurseService.postNurseCancerVisitForm).toHaveBeenCalledWith( + form, + [{ x: 1 }], + true + ); + expect(confirmation.alert).toHaveBeenCalledWith('ok', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + + it('handles 9999 status with info alert and navigates', () => { + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + nurseService.postNurseCancerVisitForm.and.returnValue( + of({ statusCode: 9999, errorMessage: 'dup' }) + ); + component.submitNurseCancerVisitDetails({}); + expect(confirmation.alert).toHaveBeenCalledWith('dup', 'info'); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('handles other status and errors', () => { + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + nurseService.postNurseCancerVisitForm.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.submitNurseCancerVisitDetails({}); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + nurseService.postNurseCancerVisitForm.and.returnValue(throwingObs('x')); + component.submitNurseCancerVisitDetails({}); + expect(confirmation.alert).toHaveBeenCalledWith('x', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + + /* ================================================================ */ + describe('getImageCoordinates', () => { + it('collects images with van & parking place ids', () => { + init(); + const form = build({ + patientExaminationForm: { + oralExaminationForm: { image: val({ id: 1 }) }, + abdominalExaminationForm: { image: val(null) }, + gynecologicalExaminationForm: { image: val({ id: 3 }) }, + breastExaminationForm: { image: val({ id: 4 }) }, + }, + }); + const res = component.getImageCoordinates(form); + expect(res.length).toBe(3); + expect(res[0]).toEqual({ id: 1, vanID: 11, parkingPlaceID: 22 }); + expect(res[2].id).toBe(4); + }); + + it('collects the abdominal image and returns empty when none', () => { + init(); + const form = build({ + patientExaminationForm: { + oralExaminationForm: { image: val(null) }, + abdominalExaminationForm: { image: val({ id: 2 }) }, + gynecologicalExaminationForm: { image: val(null) }, + breastExaminationForm: { image: val(null) }, + }, + }); + expect(component.getImageCoordinates(form)).toEqual([ + { id: 2, vanID: 11, parkingPlaceID: 22 }, + ]); + }); + }); + + /* ================================================================ */ + describe('doctor submit flows', () => { + const cases: Array<[string, string, string, string]> = [ + [ + 'submitANCDiagnosisForm', + 'postDoctorANCDetails', + 'checkNurseRequirements', + 'message', + ], + [ + 'submitNCDCareDiagnosisForm', + 'postDoctorNCDCareDetails', + 'checkNurseRequirements', + 'message', + ], + [ + 'submitCovidCareDiagnosisForm', + 'postDoctorCovidCareDetails', + 'checkNurseRequirements', + 'response', + ], + [ + 'submitNCDScreeningDiagnosisForm', + 'postDoctorNCDScreeningDetails', + 'checkNCDScreeningRequiredData', + 'message', + ], + [ + 'submitGeneralOPDDiagnosisForm', + 'postDoctorGeneralOPDDetails', + 'checkNurseRequirements', + 'message', + ], + [ + 'submitPNCDiagnosisForm', + 'postDoctorPNCDetails', + 'checkNurseRequirements', + 'message', + ], + [ + 'submitCancerDiagnosisForm', + 'postDoctorCancerVisitDetails', + 'checkCancerRequiredData', + 'message', + ], + ]; + + beforeEach(() => { + init({}, 'doctor'); + spyOn(component, 'getLabandPrescriptionData').and.returnValue([]); + }); + + cases.forEach(([method, svc, check, msgKey]) => { + describe(method, () => { + it('does not post when validation fails', () => { + spyOn(component as any, check).and.returnValue(0); + (component as any)[method](); + expect(doctorService[svc]).not.toHaveBeenCalled(); + }); + + it('navigates to doctor worklist on success', () => { + spyOn(component as any, check).and.returnValue(1); + sessionStorage.setItem('visitCode', '1'); + sessionStorage.setItem('instFlag', 'true'); + doctorService[svc].and.returnValue( + of({ statusCode: 200, data: { [msgKey]: 'done' } }) + ); + (component as any)[method](); + expect(doctorService[svc]).toHaveBeenCalled(); + expect(sessionStorage.getItem('visitCode')).toBeNull(); + expect(confirmation.alert).toHaveBeenCalledWith('done', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + }); + + it('alerts on non-200 response', () => { + spyOn(component as any, check).and.returnValue(1); + component.disableSubmitButton = true; + doctorService[svc].and.returnValue( + of({ statusCode: 200, data: null, errorMessage: 'nodata' }) + ); + (component as any)[method](); + expect(component.disableSubmitButton).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('nodata', 'error'); + }); + + it('alerts on request error', () => { + spyOn(component as any, check).and.returnValue(1); + doctorService[svc].and.returnValue(throwingObs('err')); + (component as any)[method](); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + }); + + it('NCD screening success clears inst/suspect flags', () => { + spyOn(component, 'checkNCDScreeningRequiredData').and.returnValue(true); + sessionStorage.setItem('instFlag', 'true'); + sessionStorage.setItem('suspectFlag', 'true'); + doctorService.postDoctorNCDScreeningDetails.and.returnValue( + of({ statusCode: 200, data: { message: 'm' } }) + ); + component.submitNCDScreeningDiagnosisForm(); + expect(sessionStorage.getItem('instFlag')).toBeNull(); + expect(sessionStorage.getItem('suspectFlag')).toBeNull(); + }); + }); + + /* ================================================================ */ + describe('updateDoctorDiagnosisForm', () => { + beforeEach(() => { + init({}, 'doctor'); + spyOn(component, 'getLabandPrescriptionData').and.returnValue([]); + }); + + const setCat = (cat: string) => session.store.set('visitCategory', cat); + + it('Cancer Screening: saves specialist observation (specialist)', () => { + setCat('Cancer Screening'); + component.isSpecialist = true; + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + doctorService.saveSpecialistCancerObservation.and.returnValue( + of({ statusCode: 200, data: { message: 'saved' } }) + ); + component.updateDoctorDiagnosisForm(); + expect(component.showProgressBar).toBeTrue(); + const args = + doctorService.saveSpecialistCancerObservation.calls.mostRecent().args; + expect(args[1].vanID).toBe(11); + expect(args[1].isSpecialist).toBeTrue(); + expect(confirmation.alert).toHaveBeenCalledWith('saved', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/common/tcspecialist-worklist', + ]); + }); + + it('Cancer Screening: non-specialist, failure and error paths', () => { + setCat('Cancer Screening'); + spyOn(component, 'checkCancerRequiredData').and.returnValue(true); + doctorService.saveSpecialistCancerObservation.and.returnValue( + of({ statusCode: 200, data: { message: 'saved' } }) + ); + component.updateDoctorDiagnosisForm(); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + doctorService.saveSpecialistCancerObservation.and.returnValue( + of({ statusCode: 500, errorMessage: 'bad' }) + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + doctorService.saveSpecialistCancerObservation.and.returnValue( + throwingObs('e1') + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('e1', 'error'); + expect(component.disableSubmitButton).toBeFalse(); + }); + + it('Cancer Screening: skips when invalid', () => { + setCat('Cancer Screening'); + spyOn(component, 'checkCancerRequiredData').and.returnValue(false); + component.updateDoctorDiagnosisForm(); + expect( + doctorService.saveSpecialistCancerObservation + ).not.toHaveBeenCalled(); + }); + + it('NCD screening: update, clears flags, specialist + non specialist', () => { + setCat('NCD screening'); + spyOn(component, 'checkNCDScreeningRequiredData').and.returnValue(true); + sessionStorage.setItem('instFlag', 'true'); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + of({ statusCode: 200, data: { message: 'u' } }) + ); + component.updateDoctorDiagnosisForm(); + expect( + doctorService.updateDoctorDiagnosisDetails.calls.mostRecent().args[1] + ).toBe('NCD screening'); + expect(sessionStorage.getItem('instFlag')).toBeNull(); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + component.isSpecialist = true; + component.updateDoctorDiagnosisForm(); + expect(router.navigate).toHaveBeenCalledWith([ + '/common/tcspecialist-worklist', + ]); + }); + + it('NCD screening: failure, error and invalid', () => { + setCat('NCD screening'); + const chk = spyOn( + component, + 'checkNCDScreeningRequiredData' + ).and.returnValue(true); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'no' }) + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('no', 'error'); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + throwingObs('e2') + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('e2', 'error'); + chk.and.returnValue(false); + doctorService.updateDoctorDiagnosisDetails.calls.reset(); + component.updateDoctorDiagnosisForm(); + expect(doctorService.updateDoctorDiagnosisDetails).not.toHaveBeenCalled(); + }); + + it('other categories: update success specialist / non-specialist', () => { + setCat('ANC'); + spyOn(component, 'checkNurseRequirements').and.returnValue(1); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + of({ statusCode: 200, data: { message: 'ok' } }) + ); + component.updateDoctorDiagnosisForm(); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + component.isSpecialist = true; + component.updateDoctorDiagnosisForm(); + expect(router.navigate).toHaveBeenCalledWith([ + '/common/tcspecialist-worklist', + ]); + }); + + it('other categories: failure, error and invalid', () => { + setCat('PNC'); + const chk = spyOn(component, 'checkNurseRequirements').and.returnValue(1); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'nope' }) + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('nope', 'error'); + doctorService.updateDoctorDiagnosisDetails.and.returnValue( + throwingObs('e3') + ); + component.updateDoctorDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('e3', 'error'); + chk.and.returnValue(0); + doctorService.updateDoctorDiagnosisDetails.calls.reset(); + component.updateDoctorDiagnosisForm(); + expect(doctorService.updateDoctorDiagnosisDetails).not.toHaveBeenCalled(); + }); + }); + + /* ================================================================ */ + describe('getLabandPrescriptionData', () => { + it('returns only prescribed drugs with createdBy', () => { + init(); + component.patientMedicalForm = build({ + patientCaseRecordForm: { + drugPrescriptionForm: { + prescribedDrugs: [ + { createdBy: 'd', drugName: 'A' }, + { createdBy: null, drugName: 'B' }, + ], + }, + }, + }) as FormGroup; + expect(component.getLabandPrescriptionData()).toEqual([ + { createdBy: 'd', drugName: 'A' }, + ]); + }); + }); + + /* ================================================================ */ + describe('checkNurseRequirements', () => { + const run = (cat: string, attendant: string, form: FormGroup) => { + component.visitCategory = cat; + component.attendantType = attendant; + component.patientMedicalForm = form; + return component.checkNurseRequirements(form); + }; + const notified = () => + confirmation.notify.calls.mostRecent()?.args[1] as string[]; + + beforeEach(() => { + init(); + spyOn(console, 'warn'); + }); + + it('passes for a valid General OPD nurse form', () => { + const form = nurseForm({ + exam: false, + preg: [{ pregOrder: 1, pregOutcome: { pregOutcome: 'Live Birth' } }], + allergies: [ + { allergyType: 'Food', snomedCode: '1', snomedTerm: 't' }, + { allergyType: null, snomedCode: null, snomedTerm: 't' }, + ], + }); + expect(run('General OPD', 'nurse', form)).toBe(1); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('collects PNC, vitals, examination, obstetric & allergy errors', () => { + const form = nurseForm({ + pncErr: true, + vitalsErr: true, + exam: true, + preg: [ + { + pregOrder: 1, + pregOutcome: { pregOutcome: 'Abortion' }, + abortionType: { complicationValue: 'Induced' }, + typeofFacility: REQ, + postAbortionComplication: REQ, + pregDuration: REQ, + }, + { + pregOrder: 2, + pregOutcome: { pregOutcome: 'Abortion' }, + abortionType: REQ, + typeofFacility: 'x', + postAbortionComplication: 'x', + pregDuration: 'x', + }, + ], + allergies: [{ allergyType: 'Food', snomedCode: null, snomedTerm: 't' }], + }); + component.disableSubmitButton = true; + expect(run('PNC', 'nurse', form)).toBe(0); + const req = notified(); + expect(req).toContain(L.pncData.placeofDelivery); + expect(req).toContain(L.pncData.typeofDelivery); + expect(req).toContain( + L.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.systolicBP + ); + expect(req).toContain( + L.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.pulseRate + ); + expect(req).toContain( + L.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.height + ); + expect(req).toContain( + L.ExaminationData.ANC_OPD_PNCExamination.genExamination.dangersigns + ); + expect(req).toContain( + L.ExaminationData.ANC_OPD_PNCExamination.genExamination.typeofEdema + ); + expect(req).toContain(L.allergyNameIsNotValid); + const ob = L.historyData.opdNCDPNCHistory.obstetric; + expect(req).toContain( + ob.typeofFacility + '-' + ob.orderofPregnancy + ' 1' + ); + expect(req).toContain( + ob.complicationPostAbortion + '-' + ob.orderofPregnancy + ' 1' + ); + expect(req).toContain( + ob.noOfcompletedWeeks + '-' + ob.orderofPregnancy + ' 1' + ); + expect(req).toContain( + ob.typeOfAbortion + '-' + ob.orderofPregnancy + ' 2' + ); + expect(component.disableSubmitButton).toBeFalse(); + }); + + it('flags allergy with code but no term', () => { + const form = nurseForm({ + allergies: [{ allergyType: 'Drug', snomedCode: '1', snomedTerm: null }], + }); + expect(run('NCD care', 'nurse', form)).toBe(0); + expect(notified()).toContain(L.allergyNameIsNotValid); + }); + + it('skips obstetric checks for doctor', () => { + const form = nurseForm({ + caseRecord: caseRecord(), + preg: [ + { + pregOrder: 1, + pregOutcome: { pregOutcome: 'Abortion' }, + abortionType: REQ, + typeofFacility: REQ, + postAbortionComplication: REQ, + pregDuration: REQ, + }, + ], + }); + expect(run('NCD care', 'doctor', form)).toBe(1); + }); + + it('ANC nurse: primigravida and LMP required', () => { + const form = nurseForm({ ancErr: true }); + expect(run('ANC', 'nurse', form)).toBe(0); + expect(notified()).toContain(L.ancData.ancDataDetails.primiGravida); + expect(notified()).toContain( + L.ancData.ancDataDetails.lastMenstrualPeriod + ); + }); + + it('ANC doctor: RBS and haemoglobin investigations required', () => { + component.rbsPresent = 1; + component.heamoglobinPresent = 1; + const form = nurseForm({ + ancErr: false, + caseRecord: caseRecord({ labTest: [{ procedureName: null }] }), + }); + (form.get('patientVitalsForm.rbsTestResult') as FormControl).setValue( + null + ); + expect(run('ANC', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.pleaseSelectRBSTestInInvestigation); + expect(notified()).toContain( + L.pleaseSelectHeamoglobinTestInInvestigation + ); + }); + + it('ANC doctor: passes when RBS and haemoglobin tests prescribed', () => { + component.rbsPresent = 1; + component.heamoglobinPresent = 1; + const form = nurseForm({ + ancErr: false, + caseRecord: caseRecord({ + labTest: [ + { procedureName: 'RBS Test' }, + { procedureName: 'Haemoglobin Test' }, + ], + }), + }); + expect(run('ANC', 'doctor', form)).toBe(1); + }); + + it('ANC doctor: empty lab test list with RBS result present only needs haemoglobin', () => { + component.rbsPresent = 1; + component.heamoglobinPresent = 1; + const form = nurseForm({ + ancErr: false, + caseRecord: caseRecord({ labTest: null }), + }); + expect(run('ANC', 'doctor', form)).toBe(0); + expect(notified()).not.toContain(L.pleaseSelectRBSTestInInvestigation); + expect(notified()).toContain( + L.pleaseSelectHeamoglobinTestInInvestigation + ); + }); + + it('COVID-19 nurse: comorbidity, contact, travel, symptom required', () => { + const form = nurseForm({ covidErr: true, comorbidErr: true }); + expect(run('COVID-19 Screening', 'nurse', form)).toBe(0); + const req = notified(); + expect(req).toContain( + L.historyData.ancHistory.combordityANC_OPD_NCD_PNC.comorbidConditions + ); + expect(req).toContain(L.contactHistory); + expect(req).toContain(L.covid.travelHistory); + expect(req).toContain( + L.ExaminationData.cancerScreeningExamination.symptoms.symptoms + ); + }); + + it('COVID-19 doctor: doctor diagnosis required', () => { + const form = nurseForm({ + caseRecord: caseRecord({ docDiagErr: true }), + }); + expect(run('COVID-19 Screening', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.doctorDiagnosis); + }); + + it('General OPD doctor: provisional diagnosis required', () => { + const form = nurseForm({ caseRecord: caseRecord({ provErr: true }) }); + expect(run('General OPD', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.DiagnosisDetails.provisionaldiagnosis); + }); + + it('General OPD doctor: provisional diagnosis without concept is invalid', () => { + const form = nurseForm({ caseRecord: caseRecord({ concept: '' }) }); + expect(run('General OPD', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.provisionalDiagnosisIsNotValid); + }); + + it('PNC doctor: provisional required', () => { + const form = nurseForm({ + pncErr: false, + caseRecord: caseRecord({ provErr: true }), + }); + expect(run('PNC', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.DiagnosisDetails.provisionaldiagnosis); + }); + + it('PNC doctor: provisional & confirmatory without concept invalid', () => { + const form = nurseForm({ + pncErr: false, + caseRecord: caseRecord({ + concept: null, + conf: 'x', + confConcept: undefined, + }), + }); + expect(run('PNC', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.provisionalDiagnosisIsNotValid); + expect(notified()).toContain(L.confirmatoryDiagnosisIsNotValid); + }); + + it('PNC doctor: valid diagnoses pass', () => { + const form = nurseForm({ + pncErr: false, + caseRecord: caseRecord({ conf: 'x', confConcept: '9' }), + }); + expect(run('PNC', 'doctor', form)).toBe(1); + }); + + it('Cancer Screening doctor: provisional diagnosis required', () => { + const form = nurseForm({ + caseRecord: caseRecord({ cancerDiagErr: true }), + }); + expect(run('Cancer Screening', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.DiagnosisDetails.provisionaldiagnosis); + }); + + it('NCD care: condition required', () => { + const form = nurseForm({ caseRecord: caseRecord({ ncdCond: REQ }) }); + expect(run('NCD care', 'nurse', form)).toBe(0); + expect(notified()).toContain(L.casesheet.ncdCondition); + }); + + it('NCD care: other condition requires text', () => { + const form = nurseForm({ + caseRecord: caseRecord({ ncdCond: ['Other'] }), + }); + expect(run('NCD care', 'nurse', form)).toBe(0); + expect(notified()).toContain(L.nCDConditionOther); + const ok = nurseForm({ + caseRecord: caseRecord({ ncdCond: ['Other'], ncdOther: 'y' }), + }); + confirmation.notify.calls.reset(); + expect(run('NCD care', 'nurse', ok)).toBe(1); + }); + + it('NCD care: tolerates missing case record / diagnosis form', () => { + expect(run('NCD care', 'nurse', nurseForm())).toBe(1); + const form = nurseForm({ caseRecord: { other: 1 } }); + expect(run('NCD care', 'nurse', form)).toBe(1); + }); + + it('General OPD (QC): skips history checks without vitals', () => { + const form = nurseForm({ noVitals: true }); + form.removeControl('patientHistoryForm'); + expect(run('General OPD (QC)', 'nurse', form)).toBe(1); + }); + + it('doctor: higher health care centre required when inst & suspect flags', () => { + sessionStorage.setItem('instFlag', 'true'); + sessionStorage.setItem('suspectFlag', 'true'); + const form = nurseForm({ caseRecord: caseRecord() }); + expect(run('NCD care', 'doctor', form)).toBe(0); + expect(notified()).toContain( + 'this.currentLanguageSet.Referdetails.higherhealthcarecenter' + ); + }); + + it('doctor: referral reason required for additional services', () => { + const form = nurseForm({ + caseRecord: caseRecord(), + refer: { list: ['svc'], reasonErr: true }, + }); + expect(run('NCD care', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.Referdetails.referralReason); + }); + + it('doctor: referral reason required for empty service list with institute', () => { + const form = nurseForm({ + caseRecord: caseRecord(), + refer: { list: [], inst: 'PHC', reasonErr: true }, + }); + expect(run('NCD care', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.Referdetails.referralReason); + }); + + it('doctor: referral reason required for null service list with institute', () => { + const form = nurseForm({ + caseRecord: caseRecord(), + refer: { list: null, inst: 'PHC', reasonErr: true }, + }); + expect(run('NCD care', 'doctor', form)).toBe(0); + expect(notified()).toContain(L.Referdetails.referralReason); + }); + + it('doctor: referral with reason given passes', () => { + const f1 = nurseForm({ + caseRecord: caseRecord(), + refer: { list: ['svc'] }, + }); + expect(run('NCD care', 'doctor', f1)).toBe(1); + const f2 = nurseForm({ + caseRecord: caseRecord(), + refer: { list: [], inst: 'PHC' }, + }); + expect(run('NCD care', 'doctor', f2)).toBe(1); + const f3 = nurseForm({ + caseRecord: caseRecord(), + refer: { list: null, inst: 'PHC' }, + }); + expect(run('NCD care', 'doctor', f3)).toBe(1); + }); + + it('doctor: at least one prescription required (fallback message)', () => { + const form = nurseForm({ + caseRecord: caseRecord({ drugs: [{ createdBy: null }] }), + }); + expect(run('NCD care', 'doctor', form)).toBe(0); + expect(notified()).toContain('Please add at least one prescription'); + }); + + it('doctor: prescription validation errors are caught', () => { + const cr: any = caseRecord(); + cr.drugPrescriptionForm = { prescribedDrugs: val(5) }; + const form = nurseForm({ caseRecord: cr }); + expect(run('NCD care', 'doctor', form)).toBe(1); + expect(console.warn).toHaveBeenCalled(); + }); + + it('doctor: no drug prescription form is skipped', () => { + const cr: any = caseRecord(); + delete cr.drugPrescriptionForm; + const form = nurseForm({ caseRecord: cr }); + expect(run('NCD care', 'doctor', form)).toBe(1); + }); + + describe('offline sync lung assessment', () => { + afterEach(() => (environment.isMMUOfflineSync = false)); + + it('requires lung assessment for adults when enabled', () => { + environment.isMMUOfflineSync = true; + component.enableLungAssessment = true; + component.beneficiaryAge = 30; + nurseService.isAssessmentDone = false; + expect(run('General OPD', 'nurse', nurseForm())).toBe(0); + expect(notified()).toContain('Please perform Lung Assessment'); + }); + + it('does not require lung assessment when already done', () => { + environment.isMMUOfflineSync = true; + component.enableLungAssessment = true; + component.beneficiaryAge = 30; + nurseService.isAssessmentDone = true; + expect(run('General OPD', 'nurse', nurseForm())).toBe(1); + }); + }); + }); + + /* ================================================================ */ + describe('checkCancerRequiredData', () => { + const cancerForm = (o: any = {}) => + build({ + ...(o.noVitals ? {} : { patientVitalsForm: vitals(!!o.vitalsErr) }), + patientReferForm: { + ...refer(o.refer), + }, + patientCaseRecordForm: { + provisionalDiagnosisPrimaryDoctor: e(!!o.diagErr), + }, + }) as FormGroup; + const run = (attendant: string, form: FormGroup) => { + component.visitCategory = 'Cancer Screening'; + component.attendantType = attendant; + component.patientMedicalForm = form; + return component.checkCancerRequiredData(form); + }; + + beforeEach(() => init()); + + it('returns true for valid nurse data and when vitals missing', () => { + expect(run('nurse', cancerForm())).toBeTrue(); + expect(run('nurse', cancerForm({ noVitals: true }))).toBeTrue(); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('collects vitals errors', () => { + expect(run('nurse', cancerForm({ vitalsErr: true }))).toBeFalse(); + const req = confirmation.notify.calls.mostRecent().args[1]; + expect(req.length).toBe(4); + expect(req).toContain( + L.vitalsDetails.AnthropometryDataANC_OPD_NCD_PNC.weight + ); + expect(req).toContain( + L.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.diastolicBP + ); + }); + + it('doctor: diagnosis and referral reason for service list', () => { + expect( + run( + 'doctor', + cancerForm({ diagErr: true, refer: { list: ['a'], reasonErr: true } }) + ) + ).toBeFalse(); + const req = confirmation.notify.calls.mostRecent().args[1]; + expect(req).toContain(L.DiagnosisDetails.provisionaldiagnosis); + expect(req).toContain(L.Referdetails.referralReason); + }); + + it('doctor: referral reason for empty list with institute id', () => { + expect( + run( + 'doctor', + cancerForm({ refer: { list: [], instID: 3, reasonErr: true } }) + ) + ).toBeFalse(); + }); + + it('doctor: referral reason for null list with institute id', () => { + expect( + run( + 'doctor', + cancerForm({ refer: { list: null, instID: 3, reasonErr: true } }) + ) + ).toBeFalse(); + }); + + it('doctor: valid referral combinations pass', () => { + expect(run('doctor', cancerForm({ refer: { list: ['a'] } }))).toBeTrue(); + expect( + run('doctor', cancerForm({ refer: { list: [], instID: 3 } })) + ).toBeTrue(); + expect(run('doctor', cancerForm({ refer: { list: [] } }))).toBeTrue(); + expect( + run('doctor', cancerForm({ refer: { list: null, instID: 3 } })) + ).toBeTrue(); + expect(run('doctor', cancerForm({ refer: { list: null } }))).toBeTrue(); + }); + }); + + /* ================================================================ */ + describe('TM visit details', () => { + const tmForm = (o: any) => + build({ patientVisitForm: { tmcConfirmationForm: o } }) as FormGroup; + + beforeEach(() => init()); + + it('checkTMVisitDetailsRequiredData collects errors and clears list when confirmed', () => { + const form = tmForm({ + refrredToAdditionalServiceList: val(['x']), + isTMCConfirmed: true, + tmcConfirmed: REQ, + }); + expect(component.checkTMVisitDetailsRequiredData(form)).toBeFalse(); + expect( + form.get( + 'patientVisitForm.tmcConfirmationForm.refrredToAdditionalServiceList' + )?.value + ).toBeNull(); + expect(confirmation.notify.calls.mostRecent().args[1]).toEqual([ + L.tmcConfirmed, + ]); + }); + + it('checkTMVisitDetailsRequiredData flags referred institute', () => { + const form = tmForm({ + refrredToAdditionalServiceList: REQ, + isTMCConfirmed: false, + tmcConfirmed: true, + }); + expect(component.checkTMVisitDetailsRequiredData(form)).toBeFalse(); + expect(confirmation.notify.calls.mostRecent().args[1]).toEqual([ + L.Referdetails.referredtoinstitute, + ]); + }); + + it('checkTMVisitDetailsRequiredData passes when valid', () => { + const form = tmForm({ + refrredToAdditionalServiceList: val(['x']), + isTMCConfirmed: false, + tmcConfirmed: true, + }); + expect(component.checkTMVisitDetailsRequiredData(form)).toBeTrue(); + }); + + it('submitTMPatientVisitForm posts and navigates on success', () => { + const form = tmForm({ + refrredToAdditionalServiceList: val(['x']), + isTMCConfirmed: false, + tmcConfirmed: true, + }); + doctorService.postTMReferedNurseDetails.and.returnValue( + of({ statusCode: 200, data: { response: 'tm ok' } }) + ); + component.submitTMPatientVisitForm(form); + const temp = + doctorService.postTMReferedNurseDetails.calls.mostRecent().args[1]; + expect(temp.visitCode).toBe(300); + expect(doctorService.prescribedDrugData).toBeNull(); + expect(confirmation.alert).toHaveBeenCalledWith('tm ok', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + + it('submitTMPatientVisitForm handles failure, error, invalid', () => { + const form = tmForm({ + refrredToAdditionalServiceList: val(['x']), + isTMCConfirmed: false, + tmcConfirmed: true, + }); + doctorService.postTMReferedNurseDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'tm bad' }) + ); + component.submitTMPatientVisitForm(form); + expect(confirmation.alert).toHaveBeenCalledWith('tm bad', 'error'); + doctorService.postTMReferedNurseDetails.and.returnValue( + throwingObs('tm err') + ); + component.submitTMPatientVisitForm(form); + expect(confirmation.alert).toHaveBeenCalledWith('tm err', 'error'); + spyOn(component, 'checkTMVisitDetailsRequiredData').and.returnValue( + false + ); + doctorService.postTMReferedNurseDetails.calls.reset(); + component.submitTMPatientVisitForm(form); + expect(doctorService.postTMReferedNurseDetails).not.toHaveBeenCalled(); + }); + }); + + /* ================================================================ */ + describe('checkNCDScreeningRequiredData', () => { + const ncdForm = (o: any = {}) => { + const v: any = vitals(!!o.vitalsErr); + v.rbsCheckBox = o.rbsCheck ?? false; + v.rbsTestResult = o.rbsResult === undefined ? 'ok' : o.rbsResult; + return build({ + patientVitalsForm: v, + idrsScreeningForm: { + requiredList: val( + o.requiredList === undefined ? null : o.requiredList + ), + }, + patientHistoryForm: { + physicalActivityHistory: { activityType: e(!!o.activityErr) }, + familyHistory: { + familyDiseaseList: o.family ?? [ + { + diseaseType: { diseaseType: 'Diabetes Mellitus' }, + deleted: false, + familyMembers: ['Father'], + }, + ], + }, + }, + patientCaseRecordForm: caseRecord(o.cr || {}), + patientReferForm: refer(o.refer), + }) as FormGroup; + }; + const run = (attendant: string, form: FormGroup) => { + component.visitCategory = 'NCD screening'; + component.attendantType = attendant; + component.patientMedicalForm = form; + return component.checkNCDScreeningRequiredData(form); + }; + const notified = () => confirmation.notify.calls.mostRecent().args[1]; + + beforeEach(() => { + init(); + component.beneficiary = { ageVal: 40 }; + }); + + it('passes for a valid nurse form', () => { + expect(run('nurse', ncdForm())).toBeTrue(); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('collects nurse errors (RBS, family, activity, idrs, vitals)', () => { + component.diabetesSelected = 1; + component.ncdTemperature = true; + const form = ncdForm({ + rbsCheck: true, + rbsResult: null, + activityErr: true, + vitalsErr: true, + requiredList: ['Diabetes', 'Hypertension'], + family: [ + { + diseaseType: { diseaseType: 'Hypertension' }, + deleted: false, + familyMembers: null, + }, + { diseaseType: null, deleted: false, familyMembers: null }, + ], + }); + expect(run('nurse', form)).toBeFalse(); + const req = notified(); + expect(req).toContain('Please perform RBS Test under Vitals'); + expect(req).toContain(L.pleaseSelectDiabetesMellitusInFamilyHistory); + expect(req).toContain(L.physicalActivity); + expect(req).toContain(L.familyMemberInFamilyHistory); + expect(req).toContain('Diabetes'); + expect(req).not.toContain('Hypertension'); + expect(req).toContain( + L.vitalsDetails.vitalsCancerscreening_QC.waistCircumference + ); + expect(req).toContain( + L.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.temperature + ); + expect(req).toContain( + L.vitalsDetails.vitalsDataANC_OPD_NCD_PNC.diastolicBP + ); + }); + + it('skips family checks for under-30 and temperature when not required', () => { + component.beneficiary = { ageVal: 20 }; + component.ncdTemperature = false; + const form = ncdForm({ family: [] }); + (form.get('patientVitalsForm.temperature') as FormControl).setValue(null); + (form.get('patientVitalsForm.temperature') as FormControl).setValidators( + Validators.required + ); + form.get('patientVitalsForm.temperature')?.updateValueAndValidity(); + expect(run('nurse', form)).toBeTrue(); + }); + + it('doctor: valid form with prescribed tests passes', () => { + component.rbsPresent = 1; + component.diabetesSelected = 1; + component.visualAcuityPresent = 1; + component.visualAcuityMandatory = 1; + const form = ncdForm({ + cr: { + labTest: [ + { procedureName: 'RBS Test' }, + { procedureName: 'Visual Acuity Test' }, + { procedureName: null }, + ], + }, + }); + expect(run('doctor', form)).toBeTrue(); + }); + + it('doctor: RBS under vitals or investigation & visual acuity required', () => { + component.rbsPresent = 1; + component.diabetesSelected = 1; + component.visualAcuityPresent = 1; + component.visualAcuityMandatory = 1; + const form = ncdForm({ + rbsCheck: true, + rbsResult: null, + cr: { labTest: [{ procedureName: null }] }, + }); + expect(run('doctor', form)).toBeFalse(); + expect(notified()).toContain( + 'Please select RBS Test under Vitals or Investigation' + ); + expect(notified()).toContain( + L.pleaseSelectVisualAcuityTestInInvestigation + ); + }); + + it('doctor: RBS under investigation required when checkbox unchecked', () => { + component.rbsPresent = 1; + component.diabetesSelected = 1; + component.visualAcuityPresent = 1; + component.visualAcuityMandatory = 1; + const form = ncdForm({ + rbsCheck: false, + rbsResult: null, + cr: { labTest: null }, + }); + expect(run('doctor', form)).toBeFalse(); + expect(notified()).toContain( + 'Please select RBS Test under Investigation' + ); + expect(notified()).toContain( + L.pleaseSelectVisualAcuityTestInInvestigation + ); + }); + + it('doctor: provisional diagnosis required when enabled / invalid without concept', () => { + component.enableProvisionalDiag = true; + expect(run('doctor', ncdForm({ cr: { provErr: true } }))).toBeFalse(); + expect(notified()).toContain(L.DiagnosisDetails.provisionaldiagnosis); + expect(run('doctor', ncdForm({ cr: { concept: null } }))).toBeFalse(); + expect(notified()).toContain(L.provisionalDiagnosisIsNotValid); + }); + + it('doctor: provisional error ignored when not enabled', () => { + component.enableProvisionalDiag = false; + expect(run('doctor', ncdForm({ cr: { provErr: true } }))).toBeTrue(); + }); + + it('doctor: higher health centre and referral reasons', () => { + sessionStorage.setItem('instFlag', 'true'); + sessionStorage.setItem('suspectFlag', 'true'); + expect(run('doctor', ncdForm())).toBeFalse(); + expect(notified()).toContain(L.Referdetails.higherhealthcarecenter); + sessionStorage.clear(); + expect( + run('doctor', ncdForm({ refer: { list: ['a'], reasonErr: true } })) + ).toBeFalse(); + expect(notified()).toContain(L.Referdetails.referralReason); + expect( + run( + 'doctor', + ncdForm({ refer: { list: [], inst: 'X', reasonErr: true } }) + ) + ).toBeFalse(); + expect( + run( + 'doctor', + ncdForm({ refer: { list: null, inst: 'X', reasonErr: true } }) + ) + ).toBeFalse(); + }); + + it('doctor: valid referral combinations pass', () => { + expect(run('doctor', ncdForm({ refer: { list: ['a'] } }))).toBeTrue(); + expect( + run('doctor', ncdForm({ refer: { list: [], inst: 'X' } })) + ).toBeTrue(); + expect(run('doctor', ncdForm({ refer: { list: [] } }))).toBeTrue(); + expect( + run('doctor', ncdForm({ refer: { list: null, inst: 'X' } })) + ).toBeTrue(); + }); + + it('requires lung assessment when offline sync enabled', () => { + environment.isMMUOfflineSync = true; + try { + component.enableLungAssessment = true; + component.beneficiaryAge = 50; + nurseService.isAssessmentDone = false; + expect(run('nurse', ncdForm())).toBeFalse(); + expect(notified()).toContain('Please perform Lung Assessment'); + } finally { + environment.isMMUOfflineSync = false; + } + }); + }); + + /* ================================================================ */ + describe('quick consult (doctor)', () => { + const qcForm = (o: any = {}) => { + const provList = new FormArray( + [ + build({ + provisionalDiagnosis: o.provErr ? REQ : 'flu', + conceptID: o.concept === undefined ? 'c1' : o.concept, + }), + ], + o.provListErr ? () => ({ bad: true }) : null + ); + const qc = new FormGroup({ + chiefComplaintList: new FormArray( + [ + build({ + chiefComplaint: { chiefComplaintID: 1, chiefComplaint: 'Fever' }, + }), + build({ chiefComplaint: null }), + ], + o.ccErr ? () => ({ bad: true }) : null + ), + clinicalObservation: build(o.obsErr ? REQ : 'obs'), + provisionalDiagnosisList: provList, + prescription: build({ + prescribedDrugs: [ + { createdBy: 'd', drugName: 'A' }, + { createdBy: null }, + ], + }), + test: build(o.test === undefined ? [{ name: 't1' }] : o.test), + radiology: build( + o.radiology === undefined ? [{ name: 'r1' }] : o.radiology + ), + }); + const desc: any = { + patientQuickConsultForm: qc, + patientReferForm: refer(o.refer), + }; + if (o.caseRecord) desc.patientCaseRecordForm = o.caseRecord; + return build(desc) as FormGroup; + }; + const run = ( + form: FormGroup, + attendant = 'doctor', + cat = 'General OPD (QC)' + ) => { + component.visitCategory = cat; + component.attendantType = attendant; + component.patientMedicalForm = form; + return component.checkQuickConsultDoctorData(form); + }; + const notified = () => confirmation.notify.calls.mostRecent().args[1]; + + beforeEach(() => { + init({}, 'doctor'); + spyOn(console, 'warn'); + }); + + it('checkQuickConsultDoctorData passes for valid form', () => { + expect(run(qcForm())).toBe(1); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('checkQuickConsultDoctorData collects form errors', () => { + expect( + run( + qcForm({ + ccErr: true, + obsErr: true, + provListErr: true, + provErr: true, + }) + ) + ).toBe(0); + const req = notified(); + expect(req).toContain(L.nurseData.chiefComplaintsDetails.chiefComplaints); + expect(req).toContain(L.casesheet.clinicalObs); + expect(req).toContain(L.DiagnosisDetails.provisionaldiagnosis); + }); + + it('checkQuickConsultDoctorData flags diagnosis without concept', () => { + expect(run(qcForm({ concept: null }))).toBe(0); + expect(notified()).toContain(L.provisionalDiagnosisIsNotValid); + }); + + it('checkQuickConsultDoctorData skips diagnosis item checks for nurse', () => { + expect(run(qcForm({ concept: null }), 'nurse')).toBe(1); + }); + + it('checkQuickConsultDoctorData referral reasons', () => { + expect(run(qcForm({ refer: { list: ['a'], reasonErr: true } }))).toBe(0); + expect( + run(qcForm({ refer: { list: [], inst: 'X', reasonErr: true } })) + ).toBe(0); + expect( + run(qcForm({ refer: { list: null, inst: 'X', reasonErr: true } })) + ).toBe(0); + expect(notified()).toContain(L.Referdetails.referralReason); + expect(run(qcForm({ refer: { list: ['a'] } }))).toBe(1); + expect(run(qcForm({ refer: { list: [], inst: 'X' } }))).toBe(1); + expect(run(qcForm({ refer: { list: [] } }))).toBe(1); + expect(run(qcForm({ refer: { list: null, inst: 'X' } }))).toBe(1); + }); + + it('checkQuickConsultDoctorData FP referral reason list', () => { + expect( + run( + qcForm({ refer: { list: null, inst: 'X', reasonListErr: true } }), + 'doctor', + 'FP & Contraceptive Services' + ) + ).toBe(0); + expect( + run( + qcForm({ refer: { list: null, inst: 'X' } }), + 'doctor', + 'FP & Contraceptive Services' + ) + ).toBe(1); + }); + + it('checkQuickConsultDoctorData validates prescription in case record', () => { + expect( + run( + qcForm({ + caseRecord: { drugPrescriptionForm: { prescribedDrugs: [] } }, + }) + ) + ).toBe(0); + expect(notified()).toContain('Please add at least one prescription'); + expect( + run( + qcForm({ + caseRecord: { + drugPrescriptionForm: { prescribedDrugs: [{ createdBy: 'd' }] }, + }, + }) + ) + ).toBe(1); + expect( + run( + qcForm({ + caseRecord: { drugPrescriptionForm: { prescribedDrugs: val(7) } }, + }) + ) + ).toBe(1); + expect(console.warn).toHaveBeenCalled(); + expect(run(qcForm({ caseRecord: { other: 1 } }))).toBe(1); + }); + + it('submitQuickConsultDiagnosisForm posts mapped payload (test + radiology)', () => { + const form = qcForm(); + component.patientMedicalForm = form; + component.patientReferForm = form.get('patientReferForm') as FormGroup; + spyOn(component, 'checkQuickConsultDoctorData').and.returnValue(1); + doctorService.postQuickConsultDetails.and.returnValue( + of({ statusCode: 200, data: { message: 'qc ok' } }) + ); + component.submitQuickConsultDiagnosisForm(); + const payload = + doctorService.postQuickConsultDetails.calls.mostRecent().args[0] + .quickConsultation; + expect(payload.chiefComplaintList[0]).toEqual( + jasmine.objectContaining({ + chiefComplaintID: 1, + chiefComplaint: 'Fever', + }) + ); + expect(payload.prescription.length).toBe(1); + expect(payload.labTestOrders).toEqual([{ name: 't1' }, { name: 'r1' }]); + expect(payload.refer).toEqual({ refer: true }); + expect(confirmation.alert).toHaveBeenCalledWith('qc ok', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + }); + + it('submitQuickConsultDiagnosisForm handles test-only and radiology-only', () => { + spyOn(component, 'checkQuickConsultDoctorData').and.returnValue(1); + component.patientMedicalForm = qcForm({ radiology: null }); + component.submitQuickConsultDiagnosisForm(); + expect( + doctorService.postQuickConsultDetails.calls.mostRecent().args[0] + .quickConsultation.labTestOrders + ).toEqual([{ name: 't1' }]); + component.patientMedicalForm = qcForm({ test: null }); + component.submitQuickConsultDiagnosisForm(); + expect( + doctorService.postQuickConsultDetails.calls.mostRecent().args[0] + .quickConsultation.labTestOrders + ).toEqual([{ name: 'r1' }]); + }); + + it('submitQuickConsultDiagnosisForm failure, error and invalid', () => { + const chk = spyOn( + component, + 'checkQuickConsultDoctorData' + ).and.returnValue(1); + component.patientMedicalForm = qcForm(); + doctorService.postQuickConsultDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'qc bad' }) + ); + component.submitQuickConsultDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('qc bad', 'error'); + doctorService.postQuickConsultDetails.and.returnValue(throwingObs('qe')); + component.submitQuickConsultDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('qe', 'error'); + chk.and.returnValue(0); + doctorService.postQuickConsultDetails.calls.reset(); + component.submitQuickConsultDiagnosisForm(); + expect(doctorService.postQuickConsultDetails).not.toHaveBeenCalled(); + }); + + it('mapDoctorQuickConsultDetails filters disabled tests and maps refer', () => { + component.patientMedicalForm = qcForm({ + test: [{ name: 't1', disabled: true }, { name: 't2' }], + }); + const res = component.mapDoctorQuickConsultDetails(); + expect(res.labTestOrders).toEqual([{ name: 't2' }, { name: 'r1' }]); + expect(res.prescribedDrugs.length).toBe(1); + expect(res.test).toBeUndefined(); + expect(res.refer).toEqual({ refer: true }); + const other = doctorService.postGeneralRefer.calls.mostRecent().args[1]; + expect(other.vanID).toBe(11); + expect(other.parkingPlaceID).toBe(22); + }); + + it('mapDoctorQuickConsultDetails test-only / radiology-only', () => { + component.patientMedicalForm = qcForm({ radiology: null }); + expect(component.mapDoctorQuickConsultDetails().labTestOrders).toEqual([ + { name: 't1' }, + ]); + component.patientMedicalForm = qcForm({ test: null }); + expect(component.mapDoctorQuickConsultDetails().labTestOrders).toEqual([ + { name: 'r1' }, + ]); + }); + + it('updateQuickConsultDiagnosisForm success specialist / non-specialist', () => { + component.patientMedicalForm = qcForm(); + doctorService.updateQuickConsultDetails.and.returnValue( + of({ statusCode: 200, data: { message: 'upd' } }) + ); + component.updateQuickConsultDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('upd', 'success'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + component.isSpecialist = true; + component.updateQuickConsultDiagnosisForm(); + expect(router.navigate).toHaveBeenCalledWith([ + '/common/tcspecialist-worklist', + ]); + }); + + it('updateQuickConsultDiagnosisForm failure and error', () => { + component.patientMedicalForm = qcForm(); + doctorService.updateQuickConsultDetails.and.returnValue( + of({ statusCode: 500, errorMessage: 'uf' }) + ); + component.updateQuickConsultDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('uf', 'error'); + doctorService.updateQuickConsultDetails.and.returnValue( + throwingObs('ue') + ); + component.updateQuickConsultDiagnosisForm(); + expect(confirmation.alert).toHaveBeenCalledWith('ue', 'error'); + }); + }); + + /* ================================================================ */ + describe('SMS helpers', () => { + beforeEach(() => init()); + + it('SMSObjectCreation maps diagnosis and prescriptions', () => { + component.beneficiaryRegID = 5; + const res = component.SMSObjectCreation( + [{ term: 'A' }, { term: 'B' }], + [ + { + drugName: 'Para', + dose: '1 tab', + drugStrength: '500mg', + frequency: 'BD', + duration: 3, + }, + ], + [77] + ); + expect(res.diagnosisProvided).toBe('A, B'); + expect(res.prescribedDrugs[0]).toEqual({ + beneficiaryRegID: 5, + prescribedDrugID: 77, + drugName: 'Para', + dosage: '1 tab (500mg)', + frequency: 'BD', + noOfDays: 3, + }); + }); + + it('SMSObjectCreation tolerates null inputs', () => { + const res = component.SMSObjectCreation(null, null, []); + expect(res.diagnosisProvided).toBeUndefined(); + expect(res.prescribedDrugs).toBeUndefined(); + }); + + it('sendPrescriptionSms opens dialog and navigates after close', () => { + component.sendPrescriptionSms({ a: 1 }); + expect(dialog.open).toHaveBeenCalledWith( + SmsNotificationComponent, + jasmine.objectContaining({ data: { a: 1 }, disableClose: true }) + ); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + component.isSpecialist = true; + component.sendPrescriptionSms({}); + expect(router.navigate).toHaveBeenCalledWith([ + '/common/tcspecialist-worklist', + ]); + }); + }); + + /* ================================================================ */ + describe('update-mode methods & history checks', () => { + beforeEach(() => init()); + + it('simple update mode setters', () => { + component.updatePatientVitals(); + component.updatePatientExamination(); + component.updatePatientANC(); + component.updatePatientPNC(); + expect(component.vitalsMode.toString()).toBe('update'); + expect(component.examinationMode.toString()).toBe('update'); + expect(component.ancMode.toString()).toBe('update'); + expect(component.pncMode.toString()).toBe('update'); + }); + + it('updatePatientHistory for Cancer Screening sets update directly', () => { + component.visitCategory = 'Cancer Screening'; + component.updatePatientHistory(); + expect(component.historyMode.toString()).toBe('update'); + }); + + it('updatePatientHistory for NCD screening depends on history check', () => { + component.visitCategory = 'NCD screening'; + const chk = spyOn(component, 'checkNCDScreeningHistory').and.returnValue( + 0 + ); + component.historyMode = undefined; + component.updatePatientHistory(); + expect(component.historyMode).toBeUndefined(); + chk.and.returnValue(1); + component.updatePatientHistory(); + expect(component.historyMode.toString()).toBe('update'); + }); + + it('updatePatientHistory for other categories depends on obstetric check', () => { + component.visitCategory = 'ANC'; + const chk = spyOn(component, 'checkPastObstericHistory').and.returnValue( + 0 + ); + component.historyMode = undefined; + component.updatePatientHistory(); + expect(component.historyMode).toBeUndefined(); + chk.and.returnValue(1); + component.updatePatientHistory(); + expect(component.historyMode.toString()).toBe('update'); + }); + + it('checkNCDScreeningHistory flags missing DM and family members', () => { + component.beneficiaryAge = 40; + const form = build({ + patientHistoryForm: { + familyHistory: { + familyDiseaseList: [ + { + diseaseType: { diseaseType: 'Asthma' }, + deleted: false, + familyMembers: [], + }, + { diseaseType: null, deleted: false, familyMembers: null }, + ], + }, + }, + }); + expect(component.checkNCDScreeningHistory(form)).toBe(0); + const req = confirmation.notify.calls.mostRecent().args[1]; + expect(req).toContain(L.pleaseSelectDiabetesMellitusInFamilyHistory); + expect(req).toContain(L.familyMemberInFamilyHistory); + }); + + it('checkNCDScreeningHistory passes for valid / young beneficiary', () => { + component.beneficiaryAge = 40; + const form = build({ + patientHistoryForm: { + familyHistory: { + familyDiseaseList: [ + { + diseaseType: { diseaseType: 'Diabetes Mellitus' }, + deleted: false, + familyMembers: ['Mother'], + }, + ], + }, + }, + }); + expect(component.checkNCDScreeningHistory(form)).toBe(1); + component.beneficiaryAge = 20; + const empty = build({ + patientHistoryForm: { familyHistory: { familyDiseaseList: [] } }, + }); + expect(component.checkNCDScreeningHistory(empty)).toBe(1); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('checkPastObstericHistory collects abortion and allergy errors', () => { + const form = build({ + patientHistoryForm: { + pastObstericHistory: { + pastObstericHistoryList: [ + { + pregOrder: 1, + pregOutcome: { pregOutcome: 'Abortion' }, + abortionType: { complicationValue: 'Induced' }, + typeofFacility: REQ, + postAbortionComplication: REQ, + pregDuration: REQ, + }, + { + pregOrder: 2, + pregOutcome: { pregOutcome: 'Abortion' }, + abortionType: REQ, + typeofFacility: 'x', + postAbortionComplication: 'x', + pregDuration: 'x', + }, + { pregOrder: 3, pregOutcome: null }, + ], + }, + personalHistory: { + allergicList: [ + { allergyType: 'Food', snomedCode: '1', snomedTerm: null }, + { allergyType: 'Food', snomedCode: null, snomedTerm: 'x' }, + { allergyType: null, snomedCode: null, snomedTerm: null }, + ], + }, + }, + }); + expect(component.checkPastObstericHistory(form)).toBe(0); + const req = confirmation.notify.calls.mostRecent().args[1]; + expect(req.length).toBe(5); + expect(req).toContain(L.allergyNameIsNotValid); + }); + + it('checkPastObstericHistory passes for clean history', () => { + const form = build({ + patientHistoryForm: { + pastObstericHistory: { pastObstericHistoryList: [] }, + personalHistory: { + allergicList: [ + { allergyType: 'Food', snomedCode: '1', snomedTerm: 't' }, + ], + }, + }, + }); + expect(component.checkPastObstericHistory(form)).toBe(1); + }); + + it('updatePatientNcdScreening notifies non-hypertension requirements', () => { + component.patientMedicalForm = build({ + idrsScreeningForm: { requiredList: val(['Diabetes', 'Hypertension']) }, + }) as FormGroup; + component.updatePatientNcdScreening(); + expect(confirmation.notify).toHaveBeenCalledWith( + L.alerts.info.mandatoryFields, + ['Diabetes'] + ); + expect(component.ncdScreeningMode).toBeUndefined(); + }); + + it('updatePatientNcdScreening sets update mode when nothing required', () => { + component.patientMedicalForm = build({ + idrsScreeningForm: { requiredList: val(['Hypertension']) }, + }) as FormGroup; + component.updatePatientNcdScreening(); + expect(component.ncdScreeningMode.toString()).toBe('update'); + component.ncdScreeningMode = undefined; + component.patientMedicalForm = build({ + idrsScreeningForm: { requiredList: val(null) }, + }) as FormGroup; + component.updatePatientNcdScreening(); + expect(component.ncdScreeningMode.toString()).toBe('update'); + }); + }); + + /* ================================================================ */ + describe('updatePending', () => { + const pendingForm = () => + build({ + patientANCForm: { a: 1 }, + patientHistoryForm: { a: 1 }, + patientVitalsForm: { a: 1 }, + patientExaminationForm: { a: 1 }, + idrsScreeningForm: { a: 1 }, + patientVisitForm: { covidVaccineStatusForm: { a: 1 } }, + }) as FormGroup; + const ev = (label: string) => ({ previouslySelectedStep: { label } }); + const expected = (label: string) => + L.alerts.info.dontForget + ' ' + label + ' ' + L.alerts.info.changes; + + beforeEach(() => { + init(); + component.newLookupMode = false; + component.patientMedicalForm = pendingForm(); + }); + + ['ANC', 'History', 'Vitals', 'Examination'].forEach(label => { + it(`alerts when ${label} form is dirty`, () => { + const map: any = { + ANC: 'patientANCForm', + History: 'patientHistoryForm', + Vitals: 'patientVitalsForm', + Examination: 'patientExaminationForm', + }; + component.updatePending(ev(label)); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.patientMedicalForm.get(map[label])?.markAsDirty(); + component.updatePending(ev(label)); + expect(confirmation.alert).toHaveBeenCalledWith(expected(label)); + }); + }); + + it('alerts for Vitals when update button enabled', () => { + component.enableUpdateButtonInVitals = true; + component.updatePending(ev('Vitals')); + expect(confirmation.alert).toHaveBeenCalledWith(expected('Vitals')); + }); + + it('alerts for Screening when IDRS update disabled', () => { + component.updatePending(ev('Screening')); + expect(confirmation.alert).not.toHaveBeenCalled(); + component.idrsChange(false); + expect(component.enableIDRSUpdate).toBeFalse(); + component.updatePending(ev('Screening')); + expect(confirmation.alert).toHaveBeenCalledWith(expected('Screening')); + }); + + it('alerts for Visit Details covid vaccination changes', () => { + component.updatePending(ev('Visit Details')); + expect(confirmation.alert).not.toHaveBeenCalled(); + doctorService.covidVaccineAgeGroup = '>=12 years'; + component.patientMedicalForm + .get('patientVisitForm.covidVaccineStatusForm') + ?.markAsDirty(); + component.updatePending(ev('Visit Details')); + expect(confirmation.alert).toHaveBeenCalledWith( + expected(L.covidVaccinationStatus) + ); + }); + + it('ignores unknown step labels', () => { + component.updatePending(ev('Other')); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('new lookup mode: only visit details considered', () => { + component.newLookupMode = true; + component.updatePending(ev('ANC')); + component.updatePending(ev('Visit Details')); + expect(confirmation.alert).not.toHaveBeenCalled(); + doctorService.covidVaccineAgeGroup = '>=12 years'; + doctorService.enableCovidVaccinationButton = true; + component.updatePending(ev('Visit Details')); + expect(confirmation.alert).toHaveBeenCalledWith( + expected(L.covidVaccinationStatus) + ); + }); + }); + + /* ================================================================ */ + describe('misc behaviour', () => { + beforeEach(() => init()); + + it('checkMandatory alerts when no visit category and pending files', () => { + component.visitCategory = null; + nurseService.fileData = [{ f: 1 }]; + component.checkMandatory(); + expect(confirmation.alert).toHaveBeenCalledWith( + L.alerts.info.proceedFurther + ); + expect(confirmation.alert).toHaveBeenCalledWith( + L.common.kindlyuploadthefiles + ); + expect(nurseService.fileData).toBeNull(); + }); + + it('checkMandatory does nothing when category set and no files', () => { + component.visitCategory = 'ANC'; + nurseService.fileData = []; + component.checkMandatory(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('removeBeneficiaryDataForNurseVisit / DoctorVisit clear storage', () => { + ['beneficiaryRegID', 'benFlowID', 'visitCategory', 'doctorFlag'].forEach( + k => sessionStorage.setItem(k, '1') + ); + component.removeBeneficiaryDataForNurseVisit(); + expect(sessionStorage.getItem('beneficiaryRegID')).toBeNull(); + expect(sessionStorage.getItem('visitCategory')).toBe('1'); + component.removeBeneficiaryDataForDoctorVisit(); + expect(sessionStorage.getItem('visitCategory')).toBeNull(); + expect(sessionStorage.getItem('doctorFlag')).toBeNull(); + }); + + it('resetSpinnerandEnableTheSubmitButton resets flags', () => { + component.disableSubmitButton = true; + component.showProgressBar = true; + component.resetSpinnerandEnableTheSubmitButton(); + expect(component.disableSubmitButton).toBeFalse(); + expect(component.showProgressBar).toBeFalse(); + }); + + it('getPregnancyStatus tracks pregnancy status changes', () => { + component.getPregnancyStatus(); + const vd = component.patientVisitForm.get( + 'patientVisitDetailsForm' + ) as FormGroup; + vd.patchValue({ pregnancyStatus: 'Yes' }); + expect(component.pregnancyStatus).toBe('Yes'); + vd.patchValue({ pregnancyStatus: null }); + expect(component.pregnancyStatus).toBeNull(); + }); + + it('sideNavModeChange uses over mode on small screens', () => { + const proto = Object.getPrototypeOf(window.screen); + const widthSpy = spyOnProperty(proto, 'width', 'get').and.returnValue( + 500 + ); + const sidenav = { mode: '', toggle: jasmine.createSpy('toggle') }; + component.sideNavModeChange(sidenav); + expect(sidenav.mode).toBe('over'); + widthSpy.and.returnValue(1200); + component.sideNavModeChange(sidenav); + expect(sidenav.mode).toBe('side'); + expect(sidenav.toggle).toHaveBeenCalledTimes(2); + }); + + it('canDeactivate returns true for TM case sheet', done => { + session.store.set('caseSheetTMFlag', 'true'); + component.canDeactivate().subscribe(r => { + expect(r).toBeTrue(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + done(); + }); + }); + + it('canDeactivate confirms when form dirty', () => { + sessionStorage.setItem('x', '1'); + component.patientMedicalForm.markAsDirty(); + confirmation.confirm.and.returnValue(of(false)); + let result: any; + component.canDeactivate().subscribe(r => (result = r)); + expect(result).toBeFalse(); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + L.alerts.info.navigateFurtherAlert, + 'Yes', + 'No' + ); + }); + + it('canDeactivate confirms when vitals update pending', () => { + component.enableUpdateButtonInVitals = true; + component.canDeactivate().subscribe(); + expect(confirmation.confirm).toHaveBeenCalled(); + }); + + it('canDeactivate returns true for clean form', () => { + let result: any; + component.canDeactivate().subscribe(r => (result = r)); + expect(result).toBeTrue(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('preventSubmitOnEnter prevents default', () => { + const evt = jasmine.createSpyObj('Event', ['preventDefault']); + component.preventSubmitOnEnter(evt); + expect(evt.preventDefault).toHaveBeenCalled(); + }); + + it('ngDoCheck refreshes language set', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toBe(L); + }); + + it('fetchLanguageResponse skips setValues without language', () => { + const http: any = (component as any).httpServiceService; + http.currentLangugae$ = of(null); + spyOn(component, 'setValues'); + component.fetchLanguageResponse(); + expect(component.setValues).not.toHaveBeenCalled(); + }); + + it('openBenPreviousisitDetails opens previous visit dialog', () => { + component.openBenPreviousisitDetails(); + expect(dialog.open).toHaveBeenCalledWith( + OpenPreviousVisitDetailsComponent, + jasmine.objectContaining({ data: { previous: true } }) + ); + }); + + it('ngAfterViewInit / ngAfterViewChecked run change detection', () => { + const cdr = (component as any).changeDetectorRef; + spyOn(cdr, 'detectChanges'); + component.ngAfterViewInit(); + component.ngAfterViewChecked(); + expect(cdr.detectChanges).toHaveBeenCalledTimes(2); + }); + + it('ngOnDestroy unsubscribes and clears caches', () => { + const sub = component.tmcSubmitSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + expect(component.rbsPresentSubscription.closed).toBeTrue(); + expect(component.enableVitalsButtonSubscription.closed).toBeTrue(); + expect(doctorService.clearCache).toHaveBeenCalled(); + expect(masterdataService.reset).toHaveBeenCalled(); + }); + }); + + it('ngOnDestroy tolerates missing subscriptions before init', () => { + component.ngOnDestroy(); + expect(doctorService.clearCache).toHaveBeenCalled(); + expect(masterdataService.reset).toHaveBeenCalled(); + }); +}); diff --git a/src/app/app-modules/pharmacist/dashboard/dashboard.component.spec.ts b/src/app/app-modules/pharmacist/dashboard/dashboard.component.spec.ts index bd7f10ea..162cf086 100644 --- a/src/app/app-modules/pharmacist/dashboard/dashboard.component.spec.ts +++ b/src/app/app-modules/pharmacist/dashboard/dashboard.component.spec.ts @@ -20,27 +20,31 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DashboardComponent } from './dashboard.component'; -describe('DashboardComponent', () => { +describe('pharmacist DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [DashboardComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + it('renders the shell: header with roles, router outlet and footer', () => { expect(component).toBeTruthy(); + const el: HTMLElement = fixture.nativeElement; + const header = el.querySelector('app-header'); + expect(header).not.toBeNull(); + expect(el.querySelector('router-outlet')).not.toBeNull(); + expect(el.querySelector('app-footer')).not.toBeNull(); }); }); diff --git a/src/app/app-modules/pharmacist/redir-fallback/redir-fallback.component.spec.ts b/src/app/app-modules/pharmacist/redir-fallback/redir-fallback.component.spec.ts index 1a8698b3..772f1372 100644 --- a/src/app/app-modules/pharmacist/redir-fallback/redir-fallback.component.spec.ts +++ b/src/app/app-modules/pharmacist/redir-fallback/redir-fallback.component.spec.ts @@ -20,27 +20,71 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + waitForAsync, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA, ChangeDetectorRef } from '@angular/core'; +import { Router } from '@angular/router'; import { RedirFallbackComponent } from './redir-fallback.component'; +import { ConfirmationService } from '../../core/services/confirmation.service'; describe('RedirFallbackComponent', () => { let component: RedirFallbackComponent; let fixture: ComponentFixture; + let mockConfirmationService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockChangeDetectorRef: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockChangeDetectorRef = jasmine.createSpyObj('ChangeDetectorRef', [ + 'detectChanges', + ]); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [RedirFallbackComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: Router, useValue: mockRouter }, + { provide: ChangeDetectorRef, useValue: mockChangeDetectorRef }, + ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(RedirFallbackComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngAfterViewInit', () => { + it('should alert with inventory connection error message', fakeAsync(() => { + component.ngAfterViewInit(); + tick(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Issues in connecting to Inventory, try again later', + 'error' + ); + })); + + it('should navigate to /pharmacist/pharmacist-worklist', fakeAsync(() => { + component.ngAfterViewInit(); + tick(); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/pharmacist/pharmacist-worklist', + ]); + })); + }); }); diff --git a/src/app/app-modules/pharmacist/redir-in/redir-in.component.spec.ts b/src/app/app-modules/pharmacist/redir-in/redir-in.component.spec.ts index 5d57756a..81662aab 100644 --- a/src/app/app-modules/pharmacist/redir-in/redir-in.component.spec.ts +++ b/src/app/app-modules/pharmacist/redir-in/redir-in.component.spec.ts @@ -20,27 +20,162 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; import { RedirInComponent } from './redir-in.component'; +import { ConfirmationService } from '../../core/services/confirmation.service'; describe('RedirInComponent', () => { let component: RedirInComponent; let fixture: ComponentFixture; + let mockRouter: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + + function createComponent(queryParams: any) { + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [RedirInComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: Router, useValue: mockRouter }, + { + provide: ActivatedRoute, + useValue: { queryParams: of(queryParams) }, + }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + ], }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(RedirInComponent); component = fixture.componentInstance; - fixture.detectChanges(); + } + + afterEach(() => { + sessionStorage.clear(); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('with resolve=true and currentLanguage=English', () => { + beforeEach(waitForAsync(() => { + createComponent({ resolve: 'true', currentLanguage: 'English' }); + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set setLanguage in sessionStorage to English', () => { + expect(sessionStorage.getItem('setLanguage')).toBe('English'); + }); + + it('should call confirmationService.alert with Items Dispensed', () => { + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Items Dispensed', + 'info' + ); + }); + + it('should navigate to /pharmacist/pharmacist-worklist', () => { + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/pharmacist/pharmacist-worklist', + ]); + }); + }); + + describe('with resolve=undefined', () => { + beforeEach(waitForAsync(() => { + createComponent({ resolve: 'undefined', currentLanguage: 'Hindi' }); + fixture.detectChanges(); + })); + + it('should not call confirmationService.alert when resolve is undefined', () => { + expect(mockConfirmationService.alert).not.toHaveBeenCalled(); + }); + + it('should set setLanguage in sessionStorage to Hindi', () => { + expect(sessionStorage.getItem('setLanguage')).toBe('Hindi'); + }); + + it('should still navigate to /pharmacist/pharmacist-worklist', () => { + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/pharmacist/pharmacist-worklist', + ]); + }); + }); + + describe('with currentLanguage=undefined', () => { + beforeEach(waitForAsync(() => { + createComponent({ resolve: 'false', currentLanguage: 'undefined' }); + fixture.detectChanges(); + })); + + it('should default language to English when currentLanguage is "undefined"', () => { + expect(sessionStorage.getItem('setLanguage')).toBe('English'); + }); + }); + + describe('routeToDesignation', () => { + beforeEach(waitForAsync(() => { + createComponent({ resolve: 'undefined', currentLanguage: 'English' }); + fixture.detectChanges(); + })); + + it('should navigate to /registrar/registration for Registrar', () => { + component.routeToDesignation('Registrar'); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/registrar/registration', + ]); + }); + + it('should navigate to /nurse-doctor/nurse-worklist for Nurse', () => { + component.routeToDesignation('Nurse'); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + + it('should navigate to /nurse-doctor/doctor-worklist for Doctor', () => { + component.routeToDesignation('Doctor'); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/doctor-worklist', + ]); + }); + + it('should navigate to /lab for Lab Technician', () => { + component.routeToDesignation('Lab Technician'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/lab']); + }); + + it('should navigate to /pharmacist for Pharmacist', () => { + component.routeToDesignation('Pharmacist'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/pharmacist']); + }); + + it('should navigate to /nurse-doctor/radiologist-worklist for Radiologist', () => { + component.routeToDesignation('Radiologist'); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/radiologist-worklist', + ]); + }); + + it('should navigate to /nurse-doctor/oncologist-worklist for Oncologist', () => { + component.routeToDesignation('Oncologist'); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/oncologist-worklist', + ]); + }); + + it('should not navigate for an unknown designation', () => { + mockRouter.navigate.calls.reset(); + component.routeToDesignation('Unknown'); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/app/app-modules/pharmacist/shared/services/pharmacist.service.spec.ts b/src/app/app-modules/pharmacist/shared/services/pharmacist.service.spec.ts index e1441a31..2801c625 100644 --- a/src/app/app-modules/pharmacist/shared/services/pharmacist.service.spec.ts +++ b/src/app/app-modules/pharmacist/shared/services/pharmacist.service.spec.ts @@ -20,21 +20,78 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { TestBed, inject } from '@angular/core/testing'; - +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; import { PharmacistService } from './pharmacist.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; describe('PharmacistService', () => { + let service: PharmacistService; + let httpMock: HttpTestingController; + let mockSessionStorage: jasmine.SpyObj; + beforeEach(() => { + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + TestBed.configureTestingModule({ - providers: [PharmacistService], + imports: [HttpClientTestingModule], + providers: [ + PharmacistService, + { provide: SessionStorageService, useValue: mockSessionStorage }, + ], }); + + service = TestBed.inject(PharmacistService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); }); - it('should be created', inject( - [PharmacistService], - (service: PharmacistService) => { - expect(service).toBeTruthy(); - } - )); + describe('getPharmacistWorklist', () => { + it('should make GET request with correctly built URL', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return JSON.stringify({ vanID: 10 }); + if (key === 'providerServiceID') return '100'; + if (key === 'serviceID') return '2'; + return null; + }); + + service.getPharmacistWorklist().subscribe(res => { + expect(res).toBeTruthy(); + }); + + const expectedUrl = environment.pharmacistWorklist + '100/2/10'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ statusCode: 200, data: [] }); + }); + + it('should construct URL with different van IDs', () => { + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') return JSON.stringify({ vanID: 25 }); + if (key === 'providerServiceID') return '200'; + if (key === 'serviceID') return '3'; + return null; + }); + + service.getPharmacistWorklist().subscribe(); + const expectedUrl = environment.pharmacistWorklist + '200/3/25'; + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush({ statusCode: 200 }); + }); + }); }); diff --git a/src/app/app-modules/pharmacist/worklist/worklist.component.spec.ts b/src/app/app-modules/pharmacist/worklist/worklist.component.spec.ts index 665ecbf3..e2c831d5 100644 --- a/src/app/app-modules/pharmacist/worklist/worklist.component.spec.ts +++ b/src/app/app-modules/pharmacist/worklist/worklist.component.spec.ts @@ -20,17 +20,104 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; import { WorklistComponent } from './worklist.component'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { PharmacistService } from '../shared/services/pharmacist.service'; +import { CameraService } from '../../core/services/camera.service'; +import { InventoryService } from '../../core/services/inventory.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('WorklistComponent', () => { let component: WorklistComponent; let fixture: ComponentFixture; + let mockRouter: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockPharmacistService: jasmine.SpyObj; + let mockCameraService: jasmine.SpyObj; + let mockInventoryService: jasmine.SpyObj; + let mockHttpServiceService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + + const mockWorklistResponse = { + statusCode: 200, + data: [ + { + beneficiaryID: 'BEN001', + benName: 'John Doe', + genderName: 'Male', + age: '30', + statusMessage: 'Active', + VisitCategory: 'General OPD', + benVisitNo: '1', + districtName: 'District1', + villageName: 'Village1', + preferredPhoneNum: '9876543210', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + beneficiaryRegID: 'REG001', + visitCode: 'VC001', + benFlowID: 'FL001', + }, + ], + }; + + beforeEach(waitForAsync(() => { + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['reset', 'getBeneficiaryImage'] + ); + mockPharmacistService = jasmine.createSpyObj('PharmacistService', [ + 'getPharmacistWorklist', + ]); + mockCameraService = jasmine.createSpyObj('CameraService', ['viewImage']); + mockInventoryService = jasmine.createSpyObj('InventoryService', [ + 'moveToInventory', + ]); + mockHttpServiceService = jasmine.createSpyObj('HttpServiceService', [ + 'currentLangugae$', + ]); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + (mockHttpServiceService as any).currentLangugae$ = of({ + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }); + + mockPharmacistService.getPharmacistWorklist.and.returnValue( + of(mockWorklistResponse) + ); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [WorklistComponent], + imports: [NoopAnimationsModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: Router, useValue: mockRouter }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { + provide: BeneficiaryDetailsService, + useValue: mockBeneficiaryDetailsService, + }, + { provide: PharmacistService, useValue: mockPharmacistService }, + { provide: CameraService, useValue: mockCameraService }, + { provide: InventoryService, useValue: mockInventoryService }, + { provide: HttpServiceService, useValue: mockHttpServiceService }, + { provide: SessionStorageService, useValue: mockSessionStorageService }, + ], }).compileComponents(); })); @@ -40,7 +127,435 @@ describe('WorklistComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + sessionStorage.clear(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + it('should set currentRole to Pharmacist in session storage', () => { + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith( + 'currentRole', + 'Pharmacist' + ); + }); + + it('should call removeBeneficiaryDataForVisit', () => { + spyOn(component, 'removeBeneficiaryDataForVisit'); + component.ngOnInit(); + expect(component.removeBeneficiaryDataForVisit).toHaveBeenCalled(); + }); + + it('should call loadPharmaWorklist', () => { + expect(mockPharmacistService.getPharmacistWorklist).toHaveBeenCalled(); + }); + + it('should call beneficiaryDetailsService.reset', () => { + expect(mockBeneficiaryDetailsService.reset).toHaveBeenCalled(); + }); + }); + + describe('removeBeneficiaryDataForVisit', () => { + it('should remove all expected sessionStorage items', () => { + const removeItemSpy = spyOn(sessionStorage, 'removeItem'); + component.removeBeneficiaryDataForVisit(); + + expect(removeItemSpy).toHaveBeenCalledWith('visitCode'); + expect(removeItemSpy).toHaveBeenCalledWith('beneficiaryGender'); + expect(removeItemSpy).toHaveBeenCalledWith('benFlowID'); + expect(removeItemSpy).toHaveBeenCalledWith('visitCategory'); + expect(removeItemSpy).toHaveBeenCalledWith('beneficiaryRegID'); + expect(removeItemSpy).toHaveBeenCalledWith('visitID'); + expect(removeItemSpy).toHaveBeenCalledWith('beneficiaryID'); + expect(removeItemSpy).toHaveBeenCalledWith('doctorFlag'); + expect(removeItemSpy).toHaveBeenCalledWith('nurseFlag'); + expect(removeItemSpy).toHaveBeenCalledWith('pharmacist_flag'); + expect(removeItemSpy).toHaveBeenCalledWith('caseSheetTMFlag'); + }); + }); + + describe('ngOnDestroy', () => { + it('should remove currentRole from sessionStorage', () => { + const removeItemSpy = spyOn(sessionStorage, 'removeItem'); + component.ngOnDestroy(); + expect(removeItemSpy).toHaveBeenCalledWith('currentRole'); + }); + }); + + describe('loadPharmaWorklist', () => { + it('should populate beneficiaryList on successful response', () => { + component.loadPharmaWorklist(); + expect(component.beneficiaryList).toBeDefined(); + expect(component.beneficiaryList.length).toBe(1); + }); + + it('should populate filteredBeneficiaryList on successful response', () => { + component.loadPharmaWorklist(); + expect(component.filteredBeneficiaryList).toBeDefined(); + expect(component.filteredBeneficiaryList.length).toBe(1); + }); + + it('should reset filterTerm to null on successful response', () => { + component.filterTerm = 'some search'; + component.loadPharmaWorklist(); + expect(component.filterTerm).toBeNull(); + }); + + it('should populate dataSource.data on successful response', () => { + component.loadPharmaWorklist(); + expect(component.dataSource.data.length).toBe(1); + }); + + it('should assign sno to each item in dataSource', () => { + component.loadPharmaWorklist(); + expect(component.dataSource.data[0].sno).toBe(1); + }); + + it('should alert on error response (non-200 statusCode)', () => { + const errorResponse = { + statusCode: 500, + errorMessage: 'Server Error', + data: null, + }; + mockPharmacistService.getPharmacistWorklist.and.returnValue( + of(errorResponse) + ); + component.loadPharmaWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Server Error', + 'error' + ); + }); + + it('should clear dataSource.data on error response', () => { + const errorResponse = { + statusCode: 500, + errorMessage: 'Server Error', + data: null, + }; + mockPharmacistService.getPharmacistWorklist.and.returnValue( + of(errorResponse) + ); + component.loadPharmaWorklist(); + expect(component.dataSource.data).toEqual([]); + }); + + it('should alert on subscription error', () => { + mockPharmacistService.getPharmacistWorklist.and.returnValue( + throwError('Network error') + ); + component.loadPharmaWorklist(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); + }); + + describe('loadDataToBenList', () => { + it('should fill default "Not Available" for missing genderName', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].genderName).toBe('Not Available'); + }); + + it('should fill default "Not Available" for missing age', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].age).toBe('Not Available'); + }); + + it('should fill default "Not Available" for missing statusMessage', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].statusMessage).toBe('Not Available'); + }); + + it('should fill default "Not Available" for missing VisitCategory', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].VisitCategory).toBe('Not Available'); + }); + + it('should fill default "Not Available" for missing districtName', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].districtName).toBe('Not Available'); + }); + + it('should fill default "Not Available" for missing preferredPhoneNum', () => { + const data = [{ visitDate: '2024-01-01', benVisitDate: '2024-01-01' }]; + const result = component.loadDataToBenList(data); + expect(result[0].preferredPhoneNum).toBe('Not Available'); + }); + + it('should preserve existing values when present', () => { + const data = [ + { + genderName: 'Female', + age: '25', + statusMessage: 'Done', + VisitCategory: 'NCD', + benVisitNo: '2', + districtName: 'TestDistrict', + villageName: 'TestVillage', + preferredPhoneNum: '1234567890', + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + }, + ]; + const result = component.loadDataToBenList(data); + expect(result[0].genderName).toBe('Female'); + expect(result[0].age).toBe('25'); + expect(result[0].districtName).toBe('TestDistrict'); + }); + + it('should format visitDate with moment', () => { + const data = [ + { + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + }, + ]; + const result = component.loadDataToBenList(data); + expect(result[0].visitDate).toContain('15-01-2024'); + }); + + it('should format benVisitDate with moment', () => { + const data = [ + { + visitDate: '2024-01-15T10:30:00', + benVisitDate: '2024-01-15T10:30:00', + }, + ]; + const result = component.loadDataToBenList(data); + expect(result[0].benVisitDate).toContain('15-01-2024'); + }); + }); + + describe('filterBeneficiaryList', () => { + beforeEach(() => { + component.beneficiaryList = [ + { + beneficiaryID: 'BEN001', + benName: 'John Doe', + genderName: 'Male', + age: '30', + VisitCategory: 'General OPD', + benVisitNo: '1', + districtName: 'District1', + preferredPhoneNum: '9876543210', + villageName: 'Village1', + beneficiaryRegID: 'REG001', + visitDate: '15-01-2024 10:30 AM', + }, + { + beneficiaryID: 'BEN002', + benName: 'Jane Smith', + genderName: 'Female', + age: '25', + VisitCategory: 'NCD', + benVisitNo: '2', + districtName: 'District2', + preferredPhoneNum: '1234567890', + villageName: 'Village2', + beneficiaryRegID: 'REG002', + visitDate: '16-01-2024 11:00 AM', + }, + ]; + component.filteredBeneficiaryList = [...component.beneficiaryList]; + }); + + it('should reset to full list when searchTerm is empty', () => { + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toEqual( + component.beneficiaryList + ); + }); + + it('should filter by beneficiaryID', () => { + component.filterBeneficiaryList('BEN001'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].beneficiaryID).toBe('BEN001'); + }); + + it('should filter by benName', () => { + component.filterBeneficiaryList('Jane'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('Jane Smith'); + }); + + it('should filter case-insensitively', () => { + component.filterBeneficiaryList('john'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('John Doe'); + }); + + it('should filter by genderName', () => { + component.filterBeneficiaryList('Female'); + expect(component.filteredBeneficiaryList.length).toBe(1); + }); + + it('should filter by districtName', () => { + component.filterBeneficiaryList('District2'); + expect(component.filteredBeneficiaryList.length).toBe(1); + }); + + it('should return empty list when no match is found', () => { + component.filterBeneficiaryList('ZZZZZ'); + expect(component.filteredBeneficiaryList.length).toBe(0); + }); + + it('should update dataSource.data with filtered results', () => { + component.filterBeneficiaryList('BEN001'); + expect(component.dataSource.data.length).toBe(1); + }); + }); + + describe('patientImageView', () => { + it('should call getBeneficiaryImage when benregID is provided', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'base64imagedata' }) + ); + component.patientImageView('REG001'); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).toHaveBeenCalledWith('REG001'); + }); + + it('should call cameraService.viewImage when image data exists', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'base64imagedata' }) + ); + component.patientImageView('REG001'); + expect(mockCameraService.viewImage).toHaveBeenCalledWith( + 'base64imagedata' + ); + }); + + it('should alert when image data is not found', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: null }) + ); + component.currentLanguageSet = { + alerts: { info: { imageNotFound: 'Image not found' } }, + }; + component.patientImageView('REG001'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Image not found' + ); + }); + + it('should not call getBeneficiaryImage when benregID is falsy', () => { + component.patientImageView(null); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + }); + + describe('loadPharmaPage', () => { + const mockBeneficiary = { + beneficiaryID: 'BEN001', + visitCode: 'VC001', + benFlowID: 'FL001', + beneficiaryRegID: 'REG001', + }; + + it('should call confirmationService.confirm', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.currentLanguageSet = { + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }; + component.loadPharmaPage(mockBeneficiary); + expect(mockConfirmationService.confirm).toHaveBeenCalledWith( + 'info', + 'Proceed?' + ); + }); + + it('should call inventoryService.moveToInventory when confirmed', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.currentLanguageSet = { + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }; + sessionStorage.setItem('setLanguage', 'Hindi'); + component.loadPharmaPage(mockBeneficiary); + expect(mockInventoryService.moveToInventory).toHaveBeenCalledWith( + 'BEN001', + 'VC001', + 'FL001', + 'Hindi', + 'REG001' + ); + }); + + it('passes null language when setLanguage is absent (null !== undefined, current behaviour)', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + component.currentLanguageSet = { + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }; + sessionStorage.removeItem('setLanguage'); + component.loadPharmaPage(mockBeneficiary); + expect(mockInventoryService.moveToInventory).toHaveBeenCalledWith( + 'BEN001', + 'VC001', + 'FL001', + null as any, + 'REG001' + ); + }); + + it('should not call inventoryService.moveToInventory when not confirmed', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + component.currentLanguageSet = { + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }; + component.loadPharmaPage(mockBeneficiary); + expect(mockInventoryService.moveToInventory).not.toHaveBeenCalled(); + }); + }); + + describe('pageChanged', () => { + it('should slice filteredBeneficiaryList based on event', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + component.pageChanged({ page: 1, itemsPerPage: 2 }); + expect(component.pagedList.length).toBe(2); + expect((component.pagedList[0] as any).id).toBe(1); + expect((component.pagedList[1] as any).id).toBe(2); + }); + + it('should return correct slice for page 2', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + component.pageChanged({ page: 2, itemsPerPage: 2 }); + expect(component.pagedList.length).toBe(2); + expect((component.pagedList[0] as any).id).toBe(3); + expect((component.pagedList[1] as any).id).toBe(4); + }); + }); + + describe('fetchLanguageResponse', () => { + it('should set currentLanguageSet', () => { + component.currentLanguageSet = undefined; + component.fetchLanguageResponse(); + expect(component.currentLanguageSet).toEqual({ + alerts: { info: { confirmtoProceedFurther: 'Proceed?' } }, + }); + }); + }); }); diff --git a/src/app/app-modules/registrar/dashboard/dashboard.component.spec.ts b/src/app/app-modules/registrar/dashboard/dashboard.component.spec.ts index 09b2ae60..6e8e04eb 100644 --- a/src/app/app-modules/registrar/dashboard/dashboard.component.spec.ts +++ b/src/app/app-modules/registrar/dashboard/dashboard.component.spec.ts @@ -20,17 +20,18 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DashboardComponent } from './dashboard.component'; -describe('DashboardComponent', () => { +describe('DashboardComponent (Registrar)', () => { let component: DashboardComponent; let fixture: ComponentFixture; - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [DashboardComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -40,7 +41,11 @@ describe('DashboardComponent', () => { fixture.detectChanges(); }); - it('should be created', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); + + it('should be an instance of DashboardComponent', () => { + expect(component instanceof DashboardComponent).toBe(true); + }); }); diff --git a/src/app/app-modules/registrar/registration/editRegistration.component.spec.ts b/src/app/app-modules/registrar/registration/editRegistration.component.spec.ts index 3c40b41c..81c4cdc3 100644 --- a/src/app/app-modules/registrar/registration/editRegistration.component.spec.ts +++ b/src/app/app-modules/registrar/registration/editRegistration.component.spec.ts @@ -21,282 +21,113 @@ */ import { - async, ComponentFixture, - tick, - inject, - fakeAsync, TestBed, + fakeAsync, + flush, } from '@angular/core/testing'; -import { RegistrationComponent } from './registration.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; import { ConfirmationService } from '../../core/services/confirmation.service'; -import { CameraService } from '../../core/services/camera.service'; -import { RegistrarService } from '../../../../../Common-UI/src/registrar/services/registrar.service'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { Component, Provider } from '@angular/core'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -// import { Location } from '@angular/common'; -// import { fakeAsync, tick } from '@angular/core/testing'; -// import { RouterTestingModule } from '@angular/router/testing'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../core/material.module'; -import { element } from 'protractor'; -import { RegisterOtherDetailsComponent } from './register-other-details/register-other-details.component'; -import { RegisterPersonalDetailsComponent } from './register-personal-details/register-personal-details.component'; - -class RouterStub { - navigateByUrl(url: string) { - return url; - } - navigate(url: any) { - return true; - } -} - -class RegistrarServiceMock { - // submitBeneficiary = jasmine.createSpy('submitBeneficiary() spy').andCallFake(function(){ - // return Observable.of(true); - // }); - // updateBeneficiary = jasmine.createSpy('updateBeneficiary'); - - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, - { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', - }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, - ], - }; - // benDetails = - benDetails = { - beneficiaryRegID: 7878, - firstName: 'neeraj', - lastName: 'baba', - gender: 1, - dob: 'Jan 7, 1995', - maritalStatus: 3, - income: 1, - occupation: 2, - blockID: 4209, - blockName: 'North Guwahati (Pt)', - stateID: 5, - stateName: 'Assam', - community: 4, - religion: 5, - fatherName: 'father ji', - districtID: 54, - districtName: 'KAMRUP', - villageID: 43132, - villageName: 'Nalgaon', - phoneNo: '9876543567', - literacyStatus: 'Illiterate', - motherName: 'mother ji', - govID: [], - otherGovID: [], - age: 0, - }; - - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - beneficiaryDetails = new BehaviorSubject(null); - beneficiaryDetails$ = this.beneficiaryDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } - - getPatientDataAsObservable(benRegID: any) { - this.beneficiaryDetails.next(this.benDetails); - } - - submitBeneficiary() { - return Observable.of(true); - } - updateBeneficiary() { - return Observable.of(false); - } -} - -class cameraServiceMock {} +import { SetLanguageComponent } from '../../core/components/set-language.component'; +import { RegistrarService } from '../shared/services/registrar.service'; +import { RegistrationComponent } from './registration.component'; -describe('RegisterComponent', () => { +describe('RegistrationComponent (edit beneficiary mode)', () => { let component: RegistrationComponent; let fixture: ComponentFixture; - let registrarService: RegistrarService; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], - declarations: [ - RegistrationComponent, - RegisterOtherDetailsComponent, - RegisterPersonalDetailsComponent, - ], + let registrar: any; + let router: Router; + const BEN = { beneficiaryID: '12345', firstName: 'Sita' }; + + beforeEach(async () => { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(null), + beneficiaryEditDetails$: new BehaviorSubject(BEN), + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [RegistrationComponent], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - { provide: CameraService, useClass: cameraServiceMock }, - { provide: ConfirmationService, useClass: confirmationServiceMock }, - { provide: Router, useClass: RouterStub }, + ...commonTestProviders(), + { provide: RegistrarService, useValue: registrar }, { provide: ActivatedRoute, - useValue: { snapshot: { params: { beneficiaryRegID: '7878' } } }, + useValue: { snapshot: { params: { beneficiaryID: '12345' } } }, }, + { provide: SetLanguageComponent, useValue: {} }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(RegistrationComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - spyOn(component, 'dateFormatChange').and.returnValue(1); - - // component.ngOnInit(); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture.detectChanges(); }); - afterEach(() => { - registrarService = null; - component = null; - fixture = null; - }); + afterEach(() => fixture.destroy()); - it('should create edit Beneficiary Component', () => { - expect(component).toBeTruthy(); + it('loads in revisit mode with the beneficiary data copied', () => { + expect(component.patientRevisit).toBeTrue(); + expect(component.revisitData).toEqual(BEN); + expect(component.revisitData).not.toBe(BEN); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should load form for edit Beneficiary', () => { - // component.ngOnInit(); - const form = component.beneficiaryRegistrationForm; - expect(form).toBeTruthy(); + it('does not set master data while master subject is null', () => { + expect(component.masterData).toBeUndefined(); + expect(component.govIDMaster).toBeUndefined(); }); - it('should load component in Edit Beneficiary Mode ', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(component.patientRevisit).toBe(true); + it('shows Update and Cancel buttons and hides Reset', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('#saveButton')).not.toBeNull(); + expect(el.querySelector('#cancelButton')).not.toBeNull(); + expect(el.querySelector('#resetButton')).toBeNull(); + expect(el.querySelector('h3')?.textContent).toContain( + LANGUAGE_EN.bendetails.edit + ); }); - it('should set Post button text to Update', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(buttonText).toBe('Update'); + it('update button calls updateBeneficiaryDetails', () => { + spyOn(component, 'updateBeneficiaryDetails'); + (fixture.nativeElement.querySelector('#saveButton') as HTMLElement).click(); + expect(component.updateBeneficiaryDetails).toHaveBeenCalled(); }); - it('should load Ben Data returned from the Service', () => { - const data = component.revisitData; - expect(data.beneficiaryRegID).toBe(7878); + it('cancel button calls cancelBeneficiaryChanges', () => { + const confirmation = TestBed.inject(ConfirmationService) as any; + ( + fixture.nativeElement.querySelector('#cancelButton') as HTMLElement + ).click(); + expect(confirmation.confirm).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); }); - it('should reset the form', () => { - const form = component.beneficiaryRegistrationForm; - form.markAsDirty(); - // console.log(form.dirty,'is it dirty') - fixture.detectChanges(); - component.resetBeneficiaryForm(); - fixture.detectChanges(); - expect(form.dirty).toBe(false); + it('ngOnDestroy unsubscribes and clears edit details', () => { + const sub = component.revisitDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + expect(registrar.clearBeneficiaryEditDetails).toHaveBeenCalled(); }); - it('should call the Update Service Method', () => { - const service = fixture.debugElement.injector.get(RegistrarService); - spy = spyOn(service, 'updateBeneficiary').and.callFake(function () { - return Observable.of(true); - }); - - component.updateBeneficiaryDetails(); - expect(service.updateBeneficiary).toHaveBeenCalled(); + it('ngOnDestroy skips clearing when there is no subscription', () => { + component.revisitDataSubscription = null; + component.ngOnDestroy(); + expect(registrar.clearBeneficiaryEditDetails).not.toHaveBeenCalled(); }); + + it('redirects if a different beneficiary is later emitted', fakeAsync(() => { + registrar.beneficiaryEditDetails$.next({ beneficiaryID: 'other' }); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + flush(); + })); }); diff --git a/src/app/app-modules/registrar/registration/editWrongRegistration.component.spec.ts b/src/app/app-modules/registrar/registration/editWrongRegistration.component.spec.ts index 70388425..ad56f959 100644 --- a/src/app/app-modules/registrar/registration/editWrongRegistration.component.spec.ts +++ b/src/app/app-modules/registrar/registration/editWrongRegistration.component.spec.ts @@ -20,207 +20,87 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RegistrationComponent } from './registration.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + flush, +} from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; import { ConfirmationService } from '../../core/services/confirmation.service'; -import { CameraService } from '../../core/services/camera.service'; -import { RegistrarService } from '../../../../../Common-UI/src/registrar/services/registrar.service'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -// import { Location } from '@angular/common'; -// import { fakeAsync, tick } from '@angular/core/testing'; -// import { RouterTestingModule } from '@angular/router/testing'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../core/material.module'; - -class RouterStub { - // navigateByUrl(url: string) { return url; } - - navigate(url) {} -} - -class RegistrarServiceMock { - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, - { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', - }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, - ], - }; - - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - - beneficiaryDetails = new BehaviorSubject(null); - beneficiaryDetails$ = this.beneficiaryDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } - - getPatientDataAsObservable(benRegID: any) { - //this.beneficiaryDetails.next(this.benDetails); - } -} - -class cameraServiceMock {} -class confirmationServiceMock { - alert() {} -} +import { SetLanguageComponent } from '../../core/components/set-language.component'; +import { RegistrarService } from '../shared/services/registrar.service'; +import { RegistrationComponent } from './registration.component'; -describe('RegisterComponent', () => { +describe('RegistrationComponent (edit with wrong/missing beneficiary)', () => { let component: RegistrationComponent; let fixture: ComponentFixture; - let registrarService; - - beforeEach(async(() => { + let registrar: any; + let router: Router; + let confirmation: any; + + function setup(editData: any) { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(null), + beneficiaryEditDetails$: new BehaviorSubject(editData), + }); TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], + imports: [...COMMON_TEST_IMPORTS], declarations: [RegistrationComponent], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - { provide: CameraService, useClass: cameraServiceMock }, - { provide: ConfirmationService, useClass: confirmationServiceMock }, - { - provide: Router, - useClass: class { - navigate = jasmine.createSpy('navigate'); - }, - }, + ...commonTestProviders(), + { provide: RegistrarService, useValue: registrar }, { provide: ActivatedRoute, - useValue: { snapshot: { params: { beneficiaryRegID: '7878' } } }, + useValue: { snapshot: { params: { beneficiaryID: '12345' } } }, }, + { provide: SetLanguageComponent, useValue: {} }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }); fixture = TestBed.createComponent(RegistrationComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - // component.ngOnInit(); - fixture.detectChanges(); - }); - - it('should create edit Beneficiary Component', () => { - expect(component).toBeTruthy(); - }); - - it('should load form for edit Beneficiary', () => { - // component.ngOnInit(); - const form = component.beneficiaryRegistrationForm; - expect(form).toBeTruthy(); - }); - - it('should load component in Edit Beneficiary Mode ', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(component.patientRevisit).toBe(true); - }); + router = TestBed.inject(Router); + confirmation = TestBed.inject(ConfirmationService) as any; + spyOn(router, 'navigate').and.resolveTo(true); + } - it('should set Post button text to Update', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(buttonText).toBe('Update'); - }); + afterEach(() => fixture.destroy()); - it('should redirect to Search Component as there is no Beneficiary data available', () => { - const router = fixture.debugElement.injector.get(Router); - // spyOn(router, 'navigate') + it('redirects to search with an info alert when no data is available', fakeAsync(() => { + setup(null); + fixture.detectChanges(); + expect(component.patientRevisit).toBeTrue(); + expect(component.revisitData).toBeUndefined(); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + flush(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issueInFetchDetails, + 'info' + ); + })); + it('redirects when the stored beneficiary does not match the route id', fakeAsync(() => { + setup({ beneficiaryID: '999' }); + fixture.detectChanges(); + expect(component.revisitData).toBeUndefined(); expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); - }); + flush(); + })); + + it('clears edit details on destroy', fakeAsync(() => { + setup(null); + fixture.detectChanges(); + flush(); + component.ngOnDestroy(); + expect(registrar.clearBeneficiaryEditDetails).toHaveBeenCalled(); + })); }); diff --git a/src/app/app-modules/registrar/registration/register-demographic-details/register-demographic-details.component.spec.ts b/src/app/app-modules/registrar/registration/register-demographic-details/register-demographic-details.component.spec.ts index 13388df8..c150a790 100644 --- a/src/app/app-modules/registrar/registration/register-demographic-details/register-demographic-details.component.spec.ts +++ b/src/app/app-modules/registrar/registration/register-demographic-details/register-demographic-details.component.spec.ts @@ -20,27 +20,771 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from './../../../core/services/confirmation.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { RegistrarService } from '../../shared/services/registrar.service'; +import { RegistrationUtils } from '../../shared/utility/registration-utility'; +import { RegisterEditLocationComponent } from '../register-edit-location/register-edit-location.component'; import { RegisterDemographicDetailsComponent } from './register-demographic-details.component'; +const LOCATION_DATA = { + stateID: 1, + stateName: 'Karnataka', + districtID: 10, + districtName: 'Bangalore', + blockID: 100, + blockName: 'North', + subDistrictID: 1000, + villageName: 'Hebbal', +}; +const LOCATION = { + stateMaster: [ + { stateID: 1, stateName: 'Karnataka' }, + { stateID: 2, stateName: 'Kerala' }, + ], + villageMaster: [{ districtBranchID: 1000, villageName: 'Hebbal' }], + otherLoc: { + blockID: 100, + blockName: 'North', + zoneID: 5, + zoneName: 'Zone5', + parkingPlaceID: 6, + parkingPlaceName: 'PP6', + }, +}; +const DISTRICTS = [ + { districtID: 10, districtName: 'Bangalore' }, + { districtID: 11, districtName: 'Mysore' }, +]; +const BLOCKS = [ + { blockID: 100, blockName: 'North' }, + { blockID: 101, blockName: 'South' }, +]; +const VILLAGES = [ + { districtBranchID: 1000, villageName: 'Hebbal' }, + { districtBranchID: 1001, villageName: 'Yelahanka' }, +]; +const ok = (data: any) => of({ statusCode: 200, data }); +const fail = () => of({ statusCode: 5000 }); + +const EDIT = { + beneficiaryID: 'B1', + i_bendemographics: { + habitation: 'H', + addressLine1: 'A1', + addressLine2: 'A2', + addressLine3: 'A3', + pinCode: '560001', + m_state: { stateID: 2, stateName: 'Kerala', stateCode: 'KL' }, + m_district: { districtID: 20, districtName: 'Kochi' }, + m_districtblock: { blockID: 200, blockName: 'B200' }, + m_districtbranchmapping: { districtBranchID: 2000, villageName: 'V2000' }, + zoneID: 3, + zoneName: 'Z3', + parkingPlaceID: 4, + parkingPlaceName: 'P4', + servicePointID: 7, + servicePointName: 'SP7', + }, +}; + describe('RegisterDemographicDetailsComponent', () => { let component: RegisterDemographicDetailsComponent; let fixture: ComponentFixture; + let registrar: any; + let confirmation: any; + let dialog: any; + let router: Router; + let form: FormGroup; - beforeEach(async(() => { + const village0 = () => + (form.controls['villages'] as FormArray).at(0) as FormGroup; + + function setup( + opts: { revisit?: boolean; session?: Record } = {} + ) { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject({ m: 1 }), + beneficiaryEditDetails$: new BehaviorSubject(null), + }); + registrar.getDistrictList.and.returnValue(ok(DISTRICTS)); + registrar.getSubDistrictList.and.returnValue(ok(BLOCKS)); + registrar.getVillageList.and.returnValue(ok(VILLAGES)); TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RegisterDemographicDetailsComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ + session: opts.session ?? { + locationData: JSON.stringify(LOCATION_DATA), + location: JSON.stringify(LOCATION), + servicePointID: 8, + servicePointName: 'SP8', + }, + }), + { provide: RegistrarService, useValue: registrar }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).overrideTemplate(RegisterDemographicDetailsComponent, ''); fixture = TestBed.createComponent(RegisterDemographicDetailsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + confirmation = TestBed.inject(ConfirmationService) as any; + dialog = TestBed.inject(MatDialog) as any; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + form = new RegistrationUtils( + new FormBuilder() + ).createDemographicDetailsForm(); + component.demographicDetailsForm = form; + component.patientRevisit = !!opts.revisit; + } + + afterEach(() => fixture.destroy()); + + describe('new registration', () => { + beforeEach(() => { + setup(); + fixture.detectChanges(); + }); + + it('loads language, master, location and cascades state/district/block/village', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.bendetails.editLocation + ); + expect(component.masterData).toEqual({ m: 1 }); + expect(component.locationData).toEqual(LOCATION_DATA); + expect(component.statesList).toEqual(LOCATION.stateMaster); + expect(component.zonesList).toEqual([{ zoneID: 5, zoneName: 'Zone5' }]); + expect(component.parkingPlaceList).toEqual([ + { parkingPlaceID: 6, parkingPlaceName: 'PP6' }, + ]); + expect(component.servicePointList).toEqual([ + { servicePointID: 8, servicePointName: 'SP8' }, + ]); + expect(registrar.getDistrictList).toHaveBeenCalledWith(1); + expect(registrar.getSubDistrictList).toHaveBeenCalledWith(10); + expect(registrar.getVillageList).toHaveBeenCalledWith(100); + expect(form.value).toEqual( + jasmine.objectContaining({ + stateID: 1, + stateName: 'Karnataka', + districtID: 10, + districtName: 'Bangalore', + blockID: 100, + blockName: 'North', + zoneID: 5, + parkingPlace: 6, + servicePoint: 8, + servicePointName: 'SP8', + }) + ); + expect(village0().value.villageID).toEqual({ + districtBranchID: 1000, + villageName: 'Hebbal', + }); + expect(component.villageList).toEqual(VILLAGES); + expect(component.subFilteredVillageMaster[0]).toEqual(VILLAGES); + expect(component.disableDistrict).toBeFalse(); + expect(component.demographicsVillageList).toBe(form.controls['villages']); + }); + + it('getDemographicsVillageList returns controls or null', () => { + expect(component.getDemographicsVillageList()).toBe( + (form.controls['villages'] as FormArray).controls + ); + component.demographicDetailsForm = new FormGroup({}); + expect(component.getDemographicsVillageList()).toBeNull(); + }); + + it('ngOnDestroy unsubscribes master', () => { + const sub = component.masterDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + }); + + it('ignores null master data', () => { + registrar.registrationMasterDetails$.next(null); + expect(component.masterData).toEqual({ m: 1 }); + }); + + it('ngDoCheck switches text depending on edit state', () => { + component.demographicsEditEnabled = true; + component.ngDoCheck(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.common.restoreDefault + ); + component.demographicsEditEnabled = false; + component.ngDoCheck(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.bendetails.editLocation + ); + }); + + it('setDemographicDefaults re-enables village and reloads location', () => { + village0().controls['villageID'].disable(); + component.disableState = false; + component.setDemographicDefaults(); + expect(village0().controls['villageID'].enabled).toBeTrue(); + expect(component.disableState).toBeTrue(); + expect(registrar.getDistrictList).toHaveBeenCalledTimes(2); + }); + + it('locationErrors alerts and navigates to search', () => { + component.locationErrors(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesinfetchingLocation, + 'error' + ); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + }); + + it('chooseLocations and displayVillage', () => { + expect(component.chooseLocations()).toEqual([{ id: 3, name: 'State' }]); + expect(component.displayVillage({ villageName: 'X' })).toBe('X'); + expect(component.displayVillage(null)).toBeNull(); + }); + + describe('change handlers', () => { + it('onStateChange loads districts and empties dependents', () => { + form.patchValue({ stateID: 2 }); + component.onStateChange(); + expect(form.value.stateName).toBe('Kerala'); + expect(component.districtList).toEqual(DISTRICTS); + expect(form.value.districtID).toBeNull(); + expect(form.value.blockID).toBeNull(); + }); + + it('onStateChange alerts on failure', () => { + registrar.getDistrictList.and.returnValue(fail()); + component.onStateChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + }); + + it('onDistrictChange loads sub districts', () => { + form.patchValue({ districtID: 11 }); + component.onDistrictChange(); + expect(form.value.districtName).toBe('Mysore'); + expect(component.subDistrictList).toEqual(BLOCKS); + expect(form.value.blockID).toBeNull(); + }); + + it('onDistrictChange alerts on failure', () => { + registrar.getSubDistrictList.and.returnValue(fail()); + component.onDistrictChange(); + expect(confirmation.alert).toHaveBeenCalled(); + }); + + it('onSubDistrictChange loads villages', () => { + form.patchValue({ blockID: 101 }); + component.onSubDistrictChange(); + expect(form.value.blockName).toBe('South'); + expect(component.subFilteredVillageMaster).toEqual(VILLAGES); + }); + + it('onSubDistrictChange alerts on failure', () => { + registrar.getVillageList.and.returnValue(of(null)); + component.onSubDistrictChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesinfetchingLocation, + 'error' + ); + }); + + it('updateDistrictName by name fetches sub districts', () => { + component.updateDistrictName('Mysore'); + expect(form.value.districtID).toBe(11); + expect(registrar.getSubDistrictList).toHaveBeenCalledWith(11); + expect(form.value.blockID).toBeNull(); + }); + + it('fetchSubDistrictsOnDistrictSelection alerts on failure', () => { + registrar.getSubDistrictList.and.returnValue(fail()); + component.fetchSubDistrictsOnDistrictSelection(); + expect(confirmation.alert).toHaveBeenCalled(); + }); + + it('update*Name ignore null values', () => { + form.patchValue({ stateName: 'keep', districtName: 'keep' }); + component.updateStateName(null); + component.updateDistrictName(undefined); + expect(form.value.stateName).toBe('keep'); + expect(form.value.districtName).toBe('keep'); + }); + + it('onChangeVillage populates district/taluk', () => { + registrar.getDistrictTalukList.and.returnValue( + ok([ + { + districtID: 11, + districtName: 'Mysore', + blockID: 101, + blockName: 'South', + }, + ]) + ); + component.onChangeVillage({ districtBranchID: 1000 }); + expect(registrar.getDistrictTalukList).toHaveBeenCalledWith(1000); + expect(form.value.districtID).toBe(11); + expect(form.value.blockName).toBe('South'); + expect(component.DistrictTalukList.length).toBe(1); + }); + + it('onChangeVillage alerts on failure', () => { + registrar.getDistrictTalukList.and.returnValue(fail()); + component.onChangeVillage({ districtBranchID: 1 }); + expect(confirmation.alert).toHaveBeenCalled(); + }); + }); + + describe('village suggestions', () => { + beforeEach(() => { + component.subFilteredVillageMaster = [VILLAGES]; + }); + + it('string value matching a village enables district', () => { + const g = new FormBuilder().group({ villageID: 'Hebbal' }); + component.suggestedVillageList(g, 0); + expect(component.suggestedvillageList[0]).toEqual([VILLAGES[0]]); + expect(component.disableDistrict).toBeFalse(); + expect(component.disableSubDistrict).toBeFalse(); + }); + + it('partial string disables district and empties selections', () => { + form.patchValue({ districtID: 10, blockID: 100 }); + const g = new FormBuilder().group({ villageID: 'heb' }); + component.suggestedVillageList(g, 0); + expect(component.disableDistrict).toBeTrue(); + expect(component.disableSubDistrict).toBeTrue(); + expect(form.value.districtID).toBeNull(); + expect(g.value.villageID).toBe('heb'); + }); + + it('no matches resets the village control', () => { + const g = new FormBuilder().group({ villageID: 'zzz' }); + component.suggestedVillageList(g, 0); + expect(component.suggestedvillageList[0]).toEqual([]); + expect(g.value.villageID).toBeNull(); + }); + + it('string without a filtered master uses existing suggestions', () => { + component.subFilteredVillageMaster = []; + component.suggestedvillageList[0] = [VILLAGES[1]]; + const g = new FormBuilder().group({ villageID: 'Yelahanka' }); + component.suggestedVillageList(g, 0); + expect(component.disableDistrict).toBeFalse(); + }); + + it('object value in the list still empties district (compares name to object - current behaviour)', () => { + form.patchValue({ districtID: 10, blockID: 100 }); + const g = new FormBuilder().group({ villageID: VILLAGES[1] }); + component.disableDistrict = true; + component.suggestedVillageList(g, 0); + expect(component.suggestedvillageList[0]).toEqual([VILLAGES[1]]); + // found is always false for object values, so district/block are cleared + expect(component.disableDistrict).toBeTrue(); + expect(form.value.districtID).toBeNull(); + expect(form.value.blockID).toBeNull(); + }); + + it('object value not in the list empties district', () => { + form.patchValue({ districtID: 10 }); + const g = new FormBuilder().group({ + villageID: { districtBranchID: 9, villageName: 'Hebbal' }, + }); + component.suggestedVillageList(g, 0); + expect(form.value.districtID).toBeNull(); + }); + + it('object value without filtered master keeps suggestions', () => { + component.subFilteredVillageMaster = null; + component.suggestedvillageList[0] = [VILLAGES[0]]; + const g = new FormBuilder().group({ villageID: VILLAGES[0] }); + component.suggestedVillageList(g, 0); + expect(component.disableSubDistrict).toBeFalse(); + }); + + it('filterVillage stores the selected village', () => { + component.filterVillage({ villageName: 'Yelahanka' }, 0); + expect(component.selectedvillageList[0]).toEqual(VILLAGES[1]); + // second call exercises the previous-selection branch + component.filterVillage({ villageName: 'Hebbal' }, 0); + expect(component.selectedvillageList[0]).toEqual(VILLAGES[0]); + }); + + it('filterVillage with unknown name keeps previous selection', () => { + component.selectedvillageList[0] = VILLAGES[0]; + component.suggestedvillageList[0] = []; + component.subFilteredVillageMaster = []; + component.filterVillage({ villageName: 'Nowhere' }, 0); + expect(component.selectedvillageList[0]).toEqual(VILLAGES[0]); + }); + + it('sortVillageList sorts by villageName', () => { + const list = [ + { villageName: 'b' }, + { villageName: 'a' }, + { villageName: 'a' }, + { villageName: 'c' }, + ]; + component.sortVillageList(list); + expect(list.map(v => v.villageName)).toEqual(['a', 'a', 'b', 'c']); + }); + }); + + it('updateVillageName iterates the village list', () => { + component.villageList = VILLAGES; + village0().patchValue({ villageID: { districtBranchID: 1001 } }); + expect(() => component.updateVillageName()).not.toThrow(); + // form has no top-level villageID control, so value is unchanged + expect(village0().value.villageID).toEqual({ districtBranchID: 1001 }); + }); + + describe('confirmEditDemographics (legacy choice dialog)', () => { + it('checked + state choice enables state editing', () => { + confirmation.choice.and.returnValue(of(3)); + component.confirmEditDemographics({ checked: true }); + expect(confirmation.choice).toHaveBeenCalledWith('Edit Location', [ + { id: 3, name: 'State' }, + ]); + expect(component.demographicsEditEnabled).toBeTrue(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.common.restoreDefault + ); + expect(component.disableState).toBeFalse(); + expect(form.value.stateID).toBeNull(); + expect(component.districtList).toEqual([]); + }); + + it('checked + cancelled choice restores', () => { + confirmation.choice.and.returnValue(of(null)); + component.confirmEditDemographics({ checked: true }); + expect(component.demographicsEditEnabled).toBeFalse(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.bendetails.editLocation + ); + }); + + it('unchecked + confirm restores defaults', () => { + spyOn(component, 'setDemographicDefaults'); + component.confirmEditDemographics({ checked: false }); + expect(component.demographicsEditEnabled).toBeFalse(); + expect(component.setDemographicDefaults).toHaveBeenCalled(); + }); + + it('unchecked + decline keeps editing', () => { + confirmation.confirm.and.returnValue(of(false)); + component.confirmEditDemographics({ checked: false }); + expect(component.demographicsEditEnabled).toBeTrue(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.common.restoreDefault + ); + }); + + it('neither checked nor unchecked does nothing', () => { + component.confirmEditDemographics({}); + expect(confirmation.choice).not.toHaveBeenCalled(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + }); + + describe('editReConfig', () => { + it('1 enables up to taluk', () => { + component.editReConfig(1); + expect(component.disableSubDistrict).toBeFalse(); + expect(component.villageList).toEqual([]); + expect(registrar.getSubDistrictList).toHaveBeenCalledTimes(2); + }); + it('2 enables up to district', () => { + component.editReConfig(2); + expect(component.disableDistrict).toBeFalse(); + expect(component.subDistrictList).toEqual([]); + expect(registrar.getDistrictList).toHaveBeenCalledTimes(2); + }); + it('unknown request does nothing', () => { + component.disableState = true; + component.editReConfig(99); + expect(component.disableState).toBeTrue(); + }); + }); + + describe('confirmEditDemographicsNew (edit location dialog)', () => { + const RESULT = { + data: { + villageList: VILLAGES, + selectedVillage: [{ villageID: 1001, villageName: 'Yelahanka' }], + selectedDistrict: [{ districtID: 11, districtName: 'Mysore' }], + selectedBlock: [{ blockID: 101, blockName: 'South' }], + selectedState: [{ stateID: 2, stateName: 'Kerala' }], + }, + }; + + it('applies the selected location from the dialog', () => { + dialog.open.and.returnValue({ afterClosed: () => of(RESULT) }); + component.confirmEditDemographicsNew({ checked: true }); + expect(dialog.open).toHaveBeenCalledWith(RegisterEditLocationComponent); + expect(component.demographicsEditEnabled).toBeTrue(); + expect(component.villageList).toEqual(VILLAGES); + expect(component.suggestedvillageList[0]).toEqual( + RESULT.data.selectedVillage + ); + expect(village0().getRawValue().villageID).toEqual({ + districtBranchID: 1001, + villageName: 'Yelahanka', + }); + expect(village0().controls['villageID'].disabled).toBeTrue(); + expect(form.value).toEqual( + jasmine.objectContaining({ + stateID: 2, + stateName: 'Kerala', + districtID: 11, + blockID: 101, + blockName: 'South', + }) + ); + expect(component.disableDistrict).toBeTrue(); + expect(component.disableSubDistrict).toBeTrue(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.common.restoreDefault + ); + }); + + it('dialog closed without data restores and re-enables village (new mode)', () => { + dialog.open.and.returnValue({ afterClosed: () => of(undefined) }); + village0().controls['villageID'].disable(); + component.confirmEditDemographicsNew({ checked: true }); + expect(component.demographicsEditEnabled).toBeFalse(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.bendetails.editLocation + ); + expect(village0().controls['villageID'].enabled).toBeTrue(); + }); + + it('unchecked + confirm resets flags and defaults', () => { + spyOn(component, 'setDemographicDefaults'); + component.disableVillage = true; + component.confirmEditDemographicsNew({ checked: false }); + expect(component.disableVillage).toBeFalse(); + expect(component.disableDistrict).toBeFalse(); + expect(component.setDemographicDefaults).toHaveBeenCalled(); + expect(component.demographicsEditText).toBe( + LANGUAGE_EN.bendetails.editLocation + ); + }); + + it('unchecked + decline keeps edit enabled', () => { + confirmation.confirm.and.returnValue(of(false)); + component.confirmEditDemographicsNew({ checked: false }); + expect(component.demographicsEditEnabled).toBeTrue(); + }); + + it('no checked flag does nothing', () => { + component.confirmEditDemographicsNew({}); + expect(dialog.open).not.toHaveBeenCalled(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('new registration load failures', () => { + it('alerts when districts fail to load', () => { + setup(); + registrar.getDistrictList.and.returnValue(fail()); + fixture.detectChanges(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + expect(registrar.getSubDistrictList).not.toHaveBeenCalled(); + }); + + it('alerts when sub districts fail to load', () => { + setup(); + registrar.getSubDistrictList.and.returnValue(fail()); + fixture.detectChanges(); + expect(confirmation.alert).toHaveBeenCalled(); + expect(registrar.getVillageList).not.toHaveBeenCalled(); + }); + + it('alerts when villages fail to load', () => { + setup(); + registrar.getVillageList.and.returnValue(fail()); + fixture.detectChanges(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesinfetchingLocation, + 'error' + ); + }); + }); + + describe('new registration without full location', () => { + it('uses only state master when otherLoc is missing', () => { + setup({ + session: { + locationData: JSON.stringify(LOCATION_DATA), + location: JSON.stringify({ stateMaster: LOCATION.stateMaster }), + servicePointID: 8, + servicePointName: 'SP8', + }, + }); + fixture.detectChanges(); + expect(component.statesList).toEqual(LOCATION.stateMaster); + expect(component.districtList).toEqual([]); + expect(component.subDistrictList).toEqual([]); + expect(component.villageList).toEqual([]); + expect(form.value.stateID).toBeNull(); + expect(form.value.servicePoint).toBe(8); + expect(registrar.getDistrictList).not.toHaveBeenCalled(); + }); + + it('does nothing when no state master exists', () => { + setup({ + session: { + locationData: JSON.stringify(LOCATION_DATA), + location: JSON.stringify({}), + }, + }); + fixture.detectChanges(); + expect(component.statesList).toBeUndefined(); + expect(component.servicePointList).toBeUndefined(); + }); + + it('falls back to default edit text when language lacks it', () => { + setup(); + spyOn(component, 'fetchLanguageResponse').and.callFake(() => { + component.currentLanguageSet = { + ...LANGUAGE_EN, + bendetails: { ...LANGUAGE_EN.bendetails, editLocation: '' }, + }; + }); + component.ngOnInit(); + expect(component.demographicsEditText).toBe('Edit Location'); + }); + }); + + describe('revisit (edit) mode', () => { + beforeEach(() => { + setup({ revisit: true }); + fixture.detectChanges(); + }); + + it('does not load location until edit data arrives', () => { + expect(registrar.getDistrictList).not.toHaveBeenCalled(); + expect(component.revisitData).toBeUndefined(); + }); + + it('populates the form from beneficiary edit details', () => { + registrar.beneficiaryEditDetails$.next(EDIT); + expect(component.revisitData).toEqual(EDIT); + expect(component.disableSubDistrict).toBeTrue(); + expect(component.statesList).toEqual(LOCATION.stateMaster); + expect(component.districtList).toEqual([ + { districtID: 20, districtName: 'Kochi' }, + ]); + expect(component.subDistrictList).toEqual([ + { blockID: 200, blockName: 'B200' }, + ]); + expect(component.suggestedvillageList[0]).toEqual([ + { districtBranchID: 2000, villageName: 'V2000' }, + ]); + expect(component.zonesList).toEqual([{ zoneID: 3, zoneName: 'Z3' }]); + expect(component.parkingPlaceList).toEqual([ + { parkingPlaceID: 4, parkingPlaceName: 'P4' }, + ]); + expect(component.servicePointList).toEqual([ + { servicePointID: 7, servicePointName: 'SP7' }, + ]); + expect(village0().controls['villageID'].disabled).toBeTrue(); + expect(village0().getRawValue().villageID).toEqual({ + districtBranchID: 2000, + villageName: 'V2000', + }); + expect(form.value).toEqual( + jasmine.objectContaining({ + habitation: 'H', + addressLine1: 'A1', + addressLine2: 'A2', + addressLine3: 'A3', + pincode: '560001', + stateID: 2, + stateCode: 'KL', + districtID: 20, + blockID: 200, + zoneID: 3, + parkingPlace: 4, + servicePoint: 7, + }) + ); + }); + + it('maps missing demographics to null', () => { + form.patchValue({ habitation: 'x', stateID: 9, zoneID: 9 }); + registrar.beneficiaryEditDetails$.next({ beneficiaryID: 'B2' }); + expect(form.value.habitation).toBeNull(); + expect(form.value.stateID).toBeNull(); + expect(form.value.zoneID).toBeNull(); + expect(component.districtList).toEqual([ + { districtID: null, districtName: null }, + ]); + }); + + it('keeps statesList when location has no state master', () => { + (component as any).sessionstorage.store.set( + 'location', + JSON.stringify({}) + ); + component.statesList = 'prev'; + registrar.beneficiaryEditDetails$.next(EDIT); + expect(component.statesList).toBe('prev'); + }); + + it('ignores edit data without a beneficiaryID', () => { + registrar.beneficiaryEditDetails$.next({ i_bendemographics: {} }); + expect(component.revisitData).toBeUndefined(); + }); + + it('skips defaults if revisit flag is turned off before data arrives', () => { + component.patientRevisit = false; + spyOn(component, 'loadBenEditDetails'); + registrar.beneficiaryEditDetails$.next(EDIT); + expect(component.revisitData).toEqual(EDIT); + expect(component.loadBenEditDetails).not.toHaveBeenCalled(); + }); + + it('dialog closed without data does not touch village in revisit mode', () => { + registrar.beneficiaryEditDetails$.next(EDIT); + dialog.open.and.returnValue({ afterClosed: () => of(null) }); + component.confirmEditDemographicsNew({ checked: true }); + expect(village0().controls['villageID'].disabled).toBeTrue(); + expect(component.demographicsEditEnabled).toBeFalse(); + }); + + it('ngOnDestroy unsubscribes revisit subscription', () => { + const sub = component.revisitDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + }); + + it('loadLocalMasterForDemographic is a no-op in revisit mode', () => { + component.statesList = undefined; + component.loadLocalMasterForDemographic(); + expect(component.statesList).toBeUndefined(); + }); }); }); diff --git a/src/app/app-modules/registrar/registration/register-edit-location/register-edit-location.component.spec.ts b/src/app/app-modules/registrar/registration/register-edit-location/register-edit-location.component.spec.ts index 9bb77d5b..b7c23293 100644 --- a/src/app/app-modules/registrar/registration/register-edit-location/register-edit-location.component.spec.ts +++ b/src/app/app-modules/registrar/registration/register-edit-location/register-edit-location.component.spec.ts @@ -20,27 +20,231 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from 'src/app/app-modules/core/services'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { RegistrarService } from '../../shared/services/registrar.service'; import { RegisterEditLocationComponent } from './register-edit-location.component'; +const LOCATION = { + stateMaster: [ + { stateID: 1, stateName: 'Karnataka' }, + { stateID: 2, stateName: 'Kerala' }, + ], + otherLoc: { stateID: 1, stateName: 'Karnataka' }, +}; + describe('RegisterEditLocationComponent', () => { let component: RegisterEditLocationComponent; let fixture: ComponentFixture; + let registrar: any; + let confirmation: any; + let dialogRef: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + registrar = autoSpy(RegistrarService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RegisterEditLocationComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ + session: { + location: JSON.stringify(LOCATION), + servicePointID: 3, + servicePointName: 'SP3', + }, + }), + { provide: RegistrarService, useValue: registrar }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(RegisterEditLocationComponent, '') + .compileComponents(); fixture = TestBed.createComponent(RegisterEditLocationComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService) as any; + dialogRef = TestBed.inject(MatDialogRef) as any; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + const form = () => component.demographicEditDetailsForm; + + it('initialises language, master from session and empties state', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.demographicsMaster).toEqual({ + ...LOCATION, + servicePointID: 3, + servicePointName: 'SP3', + }); + expect(component.statesList).toEqual(LOCATION.stateMaster); + expect(form().value.stateID).toBeNull(); + expect(form().value.stateName).toBeNull(); + expect(form().value.districtID).toBe(''); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); + + describe('onStateChange', () => { + it('loads districts and resets district selection', () => { + form().patchValue({ stateID: 2, districtID: 9, districtName: 'x' }); + registrar.getDistrictList.and.returnValue( + of({ statusCode: 200, data: [{ districtID: 5, districtName: 'D' }] }) + ); + component.onStateChange(); + expect(form().value.stateName).toBe('Kerala'); + expect(registrar.getDistrictList).toHaveBeenCalledWith(2); + expect(component.districtList).toEqual([ + { districtID: 5, districtName: 'D' }, + ]); + expect(form().value.districtID).toBeNull(); + expect(form().value.districtName).toBeNull(); + }); + + it('alerts on failure', () => { + registrar.getDistrictList.and.returnValue(of({ statusCode: 5000 })); + component.onStateChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + }); + + it('alerts on null response', () => { + registrar.getDistrictList.and.returnValue(of(null)); + component.onStateChange(); + expect(confirmation.alert).toHaveBeenCalled(); + }); + }); + + describe('onDistrictChange', () => { + beforeEach(() => { + component.districtList = [ + { districtID: 5, districtName: 'D5' }, + { districtID: 6, districtName: 'D6' }, + ]; + form().patchValue({ districtID: 6, blockID: 1 }); + }); + + it('loads sub districts and resets block', () => { + registrar.getSubDistrictList.and.returnValue( + of({ statusCode: 200, data: [{ blockID: 7, blockName: 'B7' }] }) + ); + component.onDistrictChange(); + expect(form().value.districtName).toBe('D6'); + expect(registrar.getSubDistrictList).toHaveBeenCalledWith(6); + expect(component.subDistrictList.length).toBe(1); + expect(form().value.blockID).toBeNull(); + }); + + it('alerts on failure', () => { + registrar.getSubDistrictList.and.returnValue(of({ statusCode: 400 })); + component.onDistrictChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + }); + }); + + describe('onSubDistrictChange', () => { + beforeEach(() => { + component.subDistrictList = [ + { blockID: 7, blockName: 'B7' }, + { blockID: 8, blockName: 'B8' }, + ]; + form().patchValue({ blockID: 7, villageID: 3 }); + }); + + it('loads villages and resets village', () => { + registrar.getVillageList.and.returnValue( + of({ + statusCode: 200, + data: [{ districtBranchID: 11, villageName: 'V' }], + }) + ); + component.onSubDistrictChange(); + expect(form().value.blockName).toBe('B7'); + expect(registrar.getVillageList).toHaveBeenCalledWith(7); + expect(component.villageList.length).toBe(1); + expect(form().value.villageID).toBeNull(); + expect(form().value.villageName).toBeNull(); + }); + + it('alerts on failure', () => { + registrar.getVillageList.and.returnValue(of({ statusCode: 500 })); + component.onSubDistrictChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingLocationDetails, + 'error' + ); + }); + }); + + it('onVillageChange sets village name', () => { + component.villageList = [ + { districtBranchID: 11, villageName: 'V11' }, + { districtBranchID: 12, villageName: 'V12' }, + ]; + form().patchValue({ villageID: 12 }); + component.onVillageChange(); + expect(form().value.villageName).toBe('V12'); + }); + + it('closeDialog closes without data', () => { + component.closeDialog(); + expect(dialogRef.close).toHaveBeenCalledWith(); + }); + + it('onSubmitEditLocation closes with the selected location', () => { + component.villageList = [{ districtBranchID: 11, villageName: 'V11' }]; + form().patchValue({ + stateID: 1, + stateName: 'Karnataka', + districtID: 5, + districtName: 'D5', + blockID: 7, + blockName: 'B7', + villageID: 11, + villageName: 'V11', + }); + component.onSubmitEditLocation(); + const expected = { + selectedState: [{ stateID: 1, stateName: 'Karnataka' }], + selectedDistrict: [{ districtID: 5, districtName: 'D5' }], + selectedBlock: [{ blockID: 7, blockName: 'B7' }], + selectedVillage: [{ villageID: 11, villageName: 'V11' }], + villageList: component.villageList, + }; + expect(component.editDetails).toEqual(expected); + expect(dialogRef.close).toHaveBeenCalledWith({ + event: 'close', + data: expected, + }); + }); + + it('trackFieldInteraction forwards to tracking', () => { + const tracking = TestBed.inject(AmritTrackingService) as any; + component.trackFieldInteraction('state'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'state', + 'Register Edit Location' + ); }); }); diff --git a/src/app/app-modules/registrar/registration/register-other-details/register-other-details.component.spec.ts b/src/app/app-modules/registrar/registration/register-other-details/register-other-details.component.spec.ts index 8c5371b0..512e9294 100644 --- a/src/app/app-modules/registrar/registration/register-other-details/register-other-details.component.spec.ts +++ b/src/app/app-modules/registrar/registration/register-other-details/register-other-details.component.spec.ts @@ -20,251 +20,532 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RegistrationComponent } from './../registration.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs'; import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; import { ConfirmationService } from '../../../core/services/confirmation.service'; -import { CameraService } from '../../../core/services/camera.service'; -import { RegistrarService } from '../../../../../../Common-UI/src/registrar/services/registrar.service'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { RegistrarService } from '../../shared/services/registrar.service'; import { RegistrationUtils } from '../../shared/utility/registration-utility'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter'; -// import { Location } from '@angular/common'; -// import { fakeAsync, tick } from '@angular/core/testing'; -// import { RouterTestingModule } from '@angular/router/testing'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../../core/material.module'; import { RegisterOtherDetailsComponent } from './register-other-details.component'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -class RouterStub { - navigateByUrl(url: string) { - return url; - } -} -class MockActivatedRoute { - snapshot = { - params: [null], - }; -} -class RegistrarServiceMock { - // submitBeneficiary = jasmine.createSpy('submitBeneficiary() spy').andCallFake(function(){ - // return Observable.of(true); - // }); - // updateBeneficiary = jasmine.createSpy('updateBeneficiary'); - - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, - { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', - }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, - ], - }; - // benDetails = - benDetails = { - beneficiaryRegID: 7878, - firstName: 'neeraj', - lastName: 'baba', - gender: 1, - dob: 'Jan 7, 1995', - maritalStatus: 3, - income: 1, - occupation: 2, - blockID: 4209, - blockName: 'North Guwahati (Pt)', - stateID: 5, - stateName: 'Assam', - community: 4, - religion: 5, - fatherName: 'father ji', - districtID: 54, - districtName: 'KAMRUP', - villageID: 43132, - villageName: 'Nalgaon', - phoneNo: '9876543567', - literacyStatus: 'Illiterate', - motherName: 'mother ji', - govID: [], - otherGovID: [], - age: 0, - }; - - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - beneficiaryDetails = new BehaviorSubject(null); - beneficiaryDetails$ = this.beneficiaryDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } - - getPatientDataAsObservable(benRegID: any) { - this.beneficiaryDetails.next(this.benDetails); - } - - submitBeneficiary() { - return Observable.of(true); - } - updateBeneficiary() { - return Observable.of(false); - } -} + +const MASTER = { + govIdEntityMaster: [ + { govtIdentityTypeID: 1, identityType: 'Aadhar' }, + { govtIdentityTypeID: 2, identityType: 'Voter ID' }, + { govtIdentityTypeID: 3, identityType: 'Driving License' }, + ], + otherGovIdEntityMaster: [ + { govtIdentityTypeID: 7, identityType: 'Health' }, + { govtIdentityTypeID: 8, identityType: 'Job' }, + ], + religionMaster: [ + { religionID: 1, religionType: 'Hindu' }, + { religionID: 7, religionType: 'Other' }, + ], + communityMaster: [ + { communityID: 1, communityType: 'General' }, + { communityID: 2, communityType: 'OBC' }, + ], +}; + +const REVISIT = { + beneficiaryID: 'B1', + fatherName: 'Dad', + motherName: 'Mom', + email: 'a@b.c', + i_bendemographics: { communityID: 2, communityName: 'OBC' }, + bankName: 'SBI', + branchName: 'Main', + ifscCode: 'IFSC', + accountNo: '123', + religionId: 1, + religion: 'Hindu', + beneficiaryIdentities: [ + { + govtIdentityTypeID: 1, + govtIdentityNo: '123412341234', + benIdentityId: 10, + deleted: false, + createdBy: 'u', + govtIdentityType: { isGovtID: true }, + }, + { + govtIdentityTypeID: 2, + govtIdentityNo: 'OLD', + benIdentityId: 11, + deleted: true, + createdBy: 'u', + govtIdentityType: { isGovtID: true }, + }, + { + govtIdentityTypeID: 7, + govtIdentityNo: 'H1', + benIdentityId: 12, + deleted: null, + createdBy: 'u', + govtIdentityType: { isGovtID: false }, + }, + ], +}; describe('RegisterOtherDetailsComponent', () => { let component: RegisterOtherDetailsComponent; let fixture: ComponentFixture; - let registrarService; - let cameraService: CameraService; - const fb = new FormBuilder(); - let confirmationService: ConfirmationService; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], + let registrar: any; + let confirmation: any; + let form: FormGroup; + let utils: RegistrationUtils; + + const govID = () => form.controls['govID'] as FormArray; + const otherGovID = () => form.controls['otherGovID'] as FormArray; + + beforeEach(async () => { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(MASTER), + beneficiaryEditDetails$: new BehaviorSubject(null), + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RegisterOtherDetailsComponent], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - CameraService, - ConfirmationService, - { provide: Router, useClass: RouterStub }, - { provide: ActivatedRoute, useClass: MockActivatedRoute }, + ...commonTestProviders(), + { provide: RegistrarService, useValue: registrar }, + { provide: SetLanguageComponent, useValue: {} }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(RegisterOtherDetailsComponent, '') + .compileComponents(); fixture = TestBed.createComponent(RegisterOtherDetailsComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - cameraService = TestBed.get(CameraService); - confirmationService = TestBed.get(ConfirmationService); - // fixture.detectChanges() - - function initGovID(): FormGroup { - return fb.group({ - type: null, - value: null, + confirmation = TestBed.inject(ConfirmationService) as any; + utils = new RegistrationUtils(new FormBuilder()); + form = utils.createOtherDetailsForm(); + component.otherDetailsForm = form; + }); + + afterEach(() => fixture.destroy()); + + describe('new registration', () => { + beforeEach(() => fixture.detectChanges()); + + it('initialises language, patterns and master data', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(component.patterns.length).toBe(6); + expect(component.patterns[0].error).toBe( + LANGUAGE_EN.common.enterDigitAadharNumber + ); + expect(component.masterData).toEqual(MASTER); + expect(component.govIDMaster[0]).toEqual(MASTER); + expect(component.otherGovIDMaster[0]).toEqual(MASTER); + expect(component.govLength).toBe(3); + expect(component.otherGovLength).toBe(2); + expect(component.otherGovIdList).toBe(govID()); + expect(component.revisitDataSubscription).toBeUndefined(); + }); + + it('ignores null master data', () => { + component.govLength = 99; + registrar.registrationMasterDetails$.next(null); + expect(component.govLength).toBe(99); + }); + + it('getGovIDControls / getOtherGovIDControls return array controls', () => { + expect(component.getGovIDControls()).toBe(govID().controls); + expect(component.getOtherGovIDControls()).toBe(otherGovID().controls); + }); + + it('returns null controls when arrays are missing', () => { + component.otherDetailsForm = new FormGroup({}); + expect(component.getGovIDControls()).toBeNull(); + expect(component.getOtherGovIDControls()).toBeNull(); + }); + + it('resetForm clears the local masters and reloads master data', () => { + component.previousGovID = [1]; + component.previousOtherGovID = [7]; + component.resetForm(); + expect(component.previousGovID).toEqual([]); + expect(component.previousOtherGovID).toEqual([]); + expect(component.govIDMaster.length).toBe(1); + expect(component.govLength).toBe(3); + expect(component.otherGovLength).toBe(2); + }); + + it('ngOnDestroy unsubscribes master subscription only when not revisit', () => { + const sub = component.masterDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + }); + + it('ngDoCheck re-applies the pattern for each selected gov ID', () => { + govID().at(0).patchValue({ type: 2 }); + component.ngDoCheck(); + expect(govID().at(0).value.maxLength).toBe(10); + expect(govID().at(0).value.allow).toBe('alphanumeric'); + }); + + it('alerting and checkIDPattern are side-effect free', () => { + spyOn(console, 'log'); + component.alerting('x'); + govID().at(0).patchValue({ idValue: '123', maxLength: 3 }); + component.checkIDPattern(0); + expect(console.log).toHaveBeenCalledWith('ok'); + (console.log as jasmine.Spy).calls.reset(); + govID().at(0).patchValue({ idValue: '12', maxLength: 3 }); + component.checkIDPattern(0); + expect(console.log).not.toHaveBeenCalledWith('ok'); + }); + }); + + describe('gov ID filtering', () => { + beforeEach(() => fixture.detectChanges()); + + it('first selection removes the ID from the next list and applies pattern', () => { + component.filtergovIDs(1, 0); + expect(component.previousGovID).toEqual([1]); + expect( + component.govIDMaster[1].govIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([2, 3]); + expect(govID().at(0).value.type).toBe(1); + expect(govID().at(0).value.maxLength).toBe(12); + }); + + it('selection at a second index removes it from other lists', () => { + component.filtergovIDs(1, 0); + govID().push(utils.initGovID()); + component.filtergovIDs(2, 1); + expect( + component.govIDMaster[0].govIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([1, 3]); + expect( + component.govIDMaster[2].govIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([3]); + }); + + it('changing a selection restores the old ID to other lists and resets value', () => { + component.filtergovIDs(1, 0); + govID().at(0).patchValue({ idValue: '1234' }); + component.filtergovIDs(3, 0); + expect(govID().at(0).value.idValue).toBeNull(); + expect(component.previousGovID[0]).toBe(3); + const ids = component.govIDMaster[1].govIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ); + expect(ids).toContain(1); + expect(ids).not.toContain(3); + expect(govID().at(0).value.minLength).toBe(8); + }); + + it('changing a selection whose previous ID is unknown pushes nothing', () => { + component.filtergovIDs(1, 0); + component.previousGovID[0] = 99; + const before = component.govIDMaster[1].govIdEntityMaster.length; + component.filtergovIDs(2, 0); + expect(component.govIDMaster[1].govIdEntityMaster.length).toBe( + before - 1 + ); + }); + + it('other gov ID first selection and change', () => { + component.filterOtherGovIDs(7, 0); + expect(component.previousOtherGovID).toEqual([7]); + expect( + component.otherGovIDMaster[1].otherGovIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([8]); + otherGovID().push(utils.initGovID()); + component.filterOtherGovIDs(8, 1); + expect( + component.otherGovIDMaster[0].otherGovIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([7]); + otherGovID().at(0).patchValue({ idValue: 'X' }); + component.filterOtherGovIDs(8, 0); + expect(otherGovID().at(0).value.idValue).toBeNull(); + expect(component.previousOtherGovID[0]).toBe(8); + expect( + component.otherGovIDMaster[1].otherGovIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toContain(7); + }); + + it('other gov ID change with unknown previous ID pushes nothing', () => { + component.filterOtherGovIDs(7, 0); + component.previousOtherGovID[0] = 99; + const before = + component.otherGovIDMaster[1].otherGovIdEntityMaster.length; + component.filterOtherGovIDs(9, 0); + expect(component.otherGovIDMaster[1].otherGovIdEntityMaster.length).toBe( + before + ); + }); + + it('addPatternforGovID ignores unknown type', () => { + component.addPatternforGovID(42, 0); + expect(govID().at(0).value.type).toBeNull(); + }); + + it('getAllowedGovChars returns allowed chars or null', () => { + expect(component.getAllowedGovChars(1) as any).toBe('number'); + expect(component.getAllowedGovChars(3) as any).toBeUndefined(); + expect(component.getAllowedGovChars(42) as any).toBeNull(); + }); + }); + + describe('addID / removeID', () => { + beforeEach(() => fixture.detectChanges()); + + it('addID pushes a new gov ID row when current one is filled', () => { + govID().at(0).patchValue({ type: 1, idValue: 'x' }); + component.addID(1, 0); + expect(govID().length).toBe(2); + }); + + it('addID alerts when gov ID row is incomplete', () => { + component.addID(1, 0); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.pleaseInputFieldFirst, + 'warn' + ); + expect(govID().length).toBe(1); + }); + + it('addID for other gov IDs', () => { + component.addID(0, 0); + expect(confirmation.alert).toHaveBeenCalled(); + otherGovID().at(0).patchValue({ type: 7, idValue: 'y' }); + component.addID(0, 0); + expect(otherGovID().length).toBe(2); + }); + + it('addID with unknown type does nothing', () => { + component.addID(5, 0); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('removeID on the only gov row clears it and restores master entry', () => { + component.filtergovIDs(1, 0); + govID().at(0).patchValue({ idValue: '1' }); + component.removeID(1, 0); + expect(govID().length).toBe(1); + expect(govID().at(0).value.type).toBeNull(); + expect(govID().at(0).value.idValue).toBeNull(); + expect(component.previousGovID).toEqual([]); + expect(component.govIDMaster.length).toBe(1); + expect( + component.govIDMaster[0].govIdEntityMaster.map( + (g: any) => g.govtIdentityTypeID + ) + ).toEqual([2, 3, 1]); + expect(component.removedGovIDs).toEqual([]); + }); + + it('removeID on second gov row removes it; revisit rows are tracked', () => { + component.patientRevisit = true; + govID().push(utils.initGovID()); + component.govIDMaster[1] = JSON.parse(JSON.stringify(MASTER)); + govID().at(1).patchValue({ type: 2, idValue: 'V', createdBy: 'u' }); + component.removeID(1, 1); + expect(govID().length).toBe(1); + expect(component.removedGovIDs.length).toBe(1); + expect(component.getRemovedIDs().removedGovIDs[0].idValue).toBe('V'); + }); + + it('removeID with empty type only removes/clears the row', () => { + govID().push(utils.initGovID()); + component.removeID(1, 1); + expect(govID().length).toBe(1); + expect(component.govIDMaster.length).toBe(1); + }); + + it('removeID for other gov IDs (single row) clears it', () => { + component.patientRevisit = true; + component.filterOtherGovIDs(7, 0); + otherGovID().at(0).patchValue({ type: 7, idValue: 'H', createdBy: 'u' }); + component.removeID(0, 0); + expect(otherGovID().length).toBe(1); + expect(otherGovID().at(0).value.type).toBeNull(); + expect(component.removedOtherGovIDs.length).toBe(1); + expect(component.getRemovedIDs().removedOtherGovIDs.length).toBe(1); + }); + + it('removeID for other gov IDs removes non-first rows', () => { + otherGovID().push(utils.initGovID()); + component.removeID(0, 1); + expect(otherGovID().length).toBe(1); + }); + + it('removeID with unknown id type does nothing', () => { + component.removeID(9, 0); + expect(govID().length).toBe(1); + expect(otherGovID().length).toBe(1); + }); + }); + + describe('revisit mode', () => { + beforeEach(() => { + component.patientRevisit = true; + fixture.detectChanges(); + }); + + it('loads edit details and non-deleted identities into the form', () => { + registrar.beneficiaryEditDetails$.next(REVISIT); + expect(component.revisitData.beneficiaryID).toBe('B1'); + expect(form.value.fatherName).toBe('Dad'); + expect(form.value.emailID).toBe('a@b.c'); + expect(form.value.community).toBe(2); + expect(form.value.communityName).toBe('OBC'); + expect(form.value.religion).toBe(1); + expect(form.value.religionOther).toBe('Hindu'); + expect(govID().length).toBe(1); + expect(govID().at(0).value).toEqual( + jasmine.objectContaining({ + type: 1, + idValue: '123412341234', + allow: 'number', + benIdentityId: 10, + deleted: false, + createdBy: 'u', + }) + ); + expect(otherGovID().length).toBe(1); + expect(otherGovID().at(0).value.idValue).toBe('H1'); + }); + + it('maps missing optional values to null', () => { + registrar.beneficiaryEditDetails$.next({ beneficiaryID: 'B2' }); + expect(form.value.fatherName).toBeNull(); + expect(form.value.community).toBeNull(); + expect(form.value.religionOther).toBeNull(); + expect(govID().length).toBe(1); + }); + + it('keeps the first empty gov row when only other IDs exist', () => { + registrar.beneficiaryEditDetails$.next({ + beneficiaryID: 'B3', + beneficiaryIdentities: [REVISIT.beneficiaryIdentities[2]], }); - } - component.otherDetailsForm = fb.group({ - fatherName: null, - motherName: null, - emailID: null, - bankName: null, - branchName: null, - IFSCCode: null, - accountNumber: null, - community: null, - religion: null, - religionOther: null, - govID: fb.array([initGovID()]), - otherGovID: fb.array([initGovID()]), - }); - component.patientRevisit = false; - - // fixture.detectChanges(); - registrarService.getRegistrationMaster(); + expect(govID().length).toBe(1); + expect(govID().at(0).value.type).toBeNull(); + expect(otherGovID().length).toBe(1); + }); + + it('ignores edit data without beneficiaryID', () => { + registrar.beneficiaryEditDetails$.next({ fatherName: 'x' }); + expect(component.revisitData).toBeUndefined(); + }); + + it('ngOnDestroy unsubscribes revisit subscription', () => { + const sub = component.revisitDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + }); + + it('configMasterForOthers skips loading when revisit flag flips off', () => { + component.patientRevisit = false; + spyOn(component, 'loadBenEditDetails'); + component.configMasterForOthers(); + registrar.beneficiaryEditDetails$.next(REVISIT); + expect(component.revisitData.beneficiaryID).toBe('B1'); + expect(component.loadBenEditDetails).not.toHaveBeenCalled(); + }); }); - it('should create other details component for new beneficiary', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); + describe('lookups', () => { + beforeEach(() => fixture.detectChanges()); + + it('getReligionName sets name, clears for Other(7)', () => { + form.patchValue({ religion: 1 }); + component.getReligionName(); + expect(form.value.religionOther).toBe('Hindu'); + form.patchValue({ religion: 7, religionOther: 'typed' }); + component.getReligionName(); + expect(form.value.religionOther).toBeNull(); + }); + + it('onCommunityChanged sets community name', () => { + form.patchValue({ community: 2 }); + component.onCommunityChanged(); + expect(form.value.communityName).toBe('OBC'); + }); + }); + + describe('checkPattern', () => { + beforeEach(() => fixture.detectChanges()); + + it('does nothing without a value', () => { + component.checkPattern(0, { idValue: null }); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('rejects a value failing the regex', () => { + govID().at(0).patchValue({ idValue: 'abc' }); + component.checkPattern(0, { + idValue: 'abcdabcdabcd', + pattern: /^\d{12}$/, + type: 1, + maxLength: 12, + error: 'bad', + }); + expect(confirmation.alert).toHaveBeenCalledWith('bad'); + expect(govID().at(0).value.idValue).toBeNull(); + }); + + it('rejects a too short driving license', () => { + component.checkPattern(0, { + idValue: 'AB12', + pattern: null, + type: 3, + minLength: 8, + error: 'short', + }); + expect(confirmation.alert).toHaveBeenCalledWith('short'); + }); + + it('rejects wrong length for other types', () => { + component.checkPattern(0, { + idValue: '1234', + type: 1, + maxLength: 12, + error: 'len', + }); + expect(confirmation.alert).toHaveBeenCalledWith('len'); + }); + + it('accepts valid values', () => { + govID().at(0).patchValue({ idValue: '123412341234' }); + component.checkPattern(0, { + idValue: '123412341234', + pattern: /^\d{12}$/, + type: 1, + maxLength: 12, + }); + component.checkPattern(0, { + idValue: 'DL123456', + pattern: undefined, + type: 3, + minLength: 8, + }); + expect(confirmation.alert).not.toHaveBeenCalled(); + expect(govID().at(0).value.idValue).toBe('123412341234'); + }); }); }); diff --git a/src/app/app-modules/registrar/registration/register-personal-details/register-personal-details.component.spec.ts b/src/app/app-modules/registrar/registration/register-personal-details/register-personal-details.component.spec.ts index f9fe9412..9b93b2e5 100644 --- a/src/app/app-modules/registrar/registration/register-personal-details/register-personal-details.component.spec.ts +++ b/src/app/app-modules/registrar/registration/register-personal-details/register-personal-details.component.spec.ts @@ -20,414 +20,718 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RegistrationComponent } from './../registration.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { BehaviorSubject, of } from 'rxjs'; +import * as moment from 'moment'; import { - FormsModule, - FormGroup, - ReactiveFormsModule, - FormBuilder, -} from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, + throwingObs, +} from 'src/testing/test-utils'; import { ConfirmationService } from '../../../core/services/confirmation.service'; import { CameraService } from '../../../core/services/camera.service'; -import { RegistrarService } from '../../../../../../Common-UI/src/registrar/services/registrar.service'; +import { BeneficiaryDetailsService } from 'src/app/app-modules/core/services'; +import { SetLanguageComponent } from 'src/app/app-modules/core/components/set-language.component'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import { RegistrarService } from '../../shared/services/registrar.service'; import { RegistrationUtils } from '../../shared/utility/registration-utility'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter'; -// import { Location } from '@angular/common'; -// import { fakeAsync, tick } from '@angular/core/testing'; -// import { RouterTestingModule } from '@angular/router/testing'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../../core/material.module'; import { RegisterPersonalDetailsComponent } from './register-personal-details.component'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -class RouterStub { - navigateByUrl(url: string) { - return url; - } -} -class MockActivatedRoute { - snapshot = { - params: [null], - }; -} -class RegistrarServiceMock { - // submitBeneficiary = jasmine.createSpy('submitBeneficiary() spy').andCallFake(function(){ - // return Observable.of(true); - // }); - // updateBeneficiary = jasmine.createSpy('updateBeneficiary'); - - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, +const MARITAL = [ + { maritalStatusID: 1, status: 'Unmarried' }, + { maritalStatusID: 2, status: 'Married' }, + { maritalStatusID: 5, status: 'Widow' }, + { maritalStatusID: 6, status: 'Widower' }, + { maritalStatusID: 7, status: 'NA' }, +]; +const MASTER = { + maritalStatusMaster: MARITAL, + genderMaster: [ + { genderID: 1, genderName: 'Male' }, + { genderID: 2, genderName: 'Female' }, + { genderID: 3, genderName: 'Transgender' }, + ], + incomeMaster: [ + { incomeStatusID: 1, incomeStatus: 'APL' }, + { incomeStatusID: 2, incomeStatus: 'BPL' }, + ], + occupationMaster: [ + { occupationID: 1, occupationType: 'Farmer' }, + { occupationID: 7, occupationType: 'Other' }, + ], + qualificationMaster: [ + { educationID: 1, educationType: 'Primary' }, + { educationID: 2, educationType: 'Graduate' }, + ], +}; + +function revisit(over: any = {}) { + return { + beneficiaryID: 'B1', + beneficiaryRegID: 77, + firstName: 'Ram', + lastName: 'K', + benAccountID: 4, + dOB: '1990-01-01T00:00:00.000Z', + benPhoneMaps: [ { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', + phoneNo: '9876543210', + parentBenRegID: 70, + benRelationshipID: 1, + benPhMapID: 3, + benRelationshipType: { benRelationshipType: 'Self' }, }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, ], + m_gender: { genderID: 1, genderName: 'Male' }, + maritalStatus: { maritalStatusID: 2, status: 'Married' }, + spouseName: 'Sita', + ageAtMarriage: 20, + literacyStatus: 'Literate', + i_bendemographics: { + incomeStatus: 'BPL', + i_beneficiaryeducation: { educationID: 2, educationType: 'Graduate' }, + occupationID: 1, + occupationName: 'Farmer', + }, + monthlyFamilyIncome: 1000, + ...over, }; - // benDetails = - benDetails = { - beneficiaryRegID: 7878, - firstName: 'neeraj', - lastName: 'baba', - gender: 1, - dob: 'Jan 7, 1995', - maritalStatus: 3, - income: 1, - occupation: 2, - blockID: 4209, - blockName: 'North Guwahati (Pt)', - stateID: 5, - stateName: 'Assam', - community: 4, - religion: 5, - fatherName: 'father ji', - districtID: 54, - districtName: 'KAMRUP', - villageID: 43132, - villageName: 'Nalgaon', - phoneNo: '9876543567', - literacyStatus: 'Illiterate', - motherName: 'mother ji', - govID: [], - otherGovID: [], - age: 0, - }; - - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - beneficiaryDetails = new BehaviorSubject(null); - beneficiaryDetails$ = this.beneficiaryDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } - - getPatientDataAsObservable(benRegID: any) { - this.beneficiaryDetails.next(this.benDetails); - } - - submitBeneficiary() { - return Observable.of(true); - } - updateBeneficiary() { - return Observable.of(false); - } } describe('RegisterPersonalDetailsComponent', () => { let component: RegisterPersonalDetailsComponent; let fixture: ComponentFixture; - let registrarService; - let cameraService: CameraService; - const fb = new FormBuilder(); - let confirmationService: ConfirmationService; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], + let registrar: any; + let confirmation: any; + let camera: any; + let benDetails: any; + let tracking: any; + let form: FormGroup; + + beforeEach(async () => { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(null), + beneficiaryEditDetails$: new BehaviorSubject(null), + }); + camera = { + capture: jasmine.createSpy('capture').and.returnValue(of('img')), + }; + benDetails = autoSpy(BeneficiaryDetailsService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RegisterPersonalDetailsComponent], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - CameraService, - ConfirmationService, - { provide: Router, useClass: RouterStub }, - { provide: ActivatedRoute, useClass: MockActivatedRoute }, + ...commonTestProviders(), + { provide: RegistrarService, useValue: registrar }, + { provide: CameraService, useValue: camera }, + { provide: BeneficiaryDetailsService, useValue: benDetails }, + { provide: SetLanguageComponent, useValue: {} }, ], - }).compileComponents(); - })); - - beforeEach(() => { + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(RegisterPersonalDetailsComponent, '') + .compileComponents(); fixture = TestBed.createComponent(RegisterPersonalDetailsComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - cameraService = TestBed.get(CameraService); - confirmationService = TestBed.get(ConfirmationService); - // fixture.detectChanges() - component.personalDetailsForm = fb.group({ - firstName: null, - lastName: null, - gender: null, - dob: null, - maritalStatus: null, - image: null, - spouseName: null, - aadharNo: null, - income: null, - literacyStatus: null, - educationQualification: null, - occupation: null, - occupationOther: null, - phoneNo: null, - age: null, - ageUnit: null, - ageAtMarriage: null, - fingerprint: null, - }); - component.patientRevisit = false; - - // fixture.detectChanges(); - registrarService.getRegistrationMaster(); + confirmation = TestBed.inject(ConfirmationService) as any; + tracking = TestBed.inject(AmritTrackingService) as any; + form = new RegistrationUtils(new FormBuilder()).createPersonalDetailsForm(); + component.personalDetailsForm = form; }); - it('should create personal details component for new beneficiary', () => { - component.ngOnInit(); - expect(component).toBeTruthy(); - }); + afterEach(() => fixture.destroy()); + + describe('ngOnInit (new registration)', () => { + beforeEach(() => fixture.detectChanges()); + + it('sets defaults, limits, language and calendar config', () => { + expect(component.isMobileNoRequired).toBeTrue(); + expect(component.isOccuptionRequired).toBeTrue(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(form.value.ageUnit).toBe('Years'); + expect(form.value.checked).toBeTrue(); + expect(component.today).toEqual(jasmine.any(Date)); + expect( + component.today.getFullYear() - component.minDate.getFullYear() + ).toBe(121); + expect(component.bsConfig).toEqual({ + containerClass: 'theme-dark-blue', + dateInputFormat: 'DD/MM/YYYY', + showWeekNumbers: false, + }); + expect(component.masterData).toBeUndefined(); + }); - it("should show patientRevisit as 'false' because its a new patient", () => { - component.patientRevisit = false; - component.ngOnInit(); - expect(component.patientRevisit).toBe(false); + it('stores master data but does not load edit data when not revisit', () => { + registrar.registrationMasterDetails$.next(MASTER); + expect(component.masterData).toBe(MASTER); + expect(component.revisitDataSubscription).toBeUndefined(); + }); + + it('ngOnDestroy unsubscribes master subscription', () => { + const sub = component.masterDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); }); - it("should show patientRevisit as 'true' because its a Revisiting patient", () => { - component.patientRevisit = true; - component.ngOnInit(); - expect(component.patientRevisit).toBe(true); + describe('revisit mode', () => { + beforeEach(() => { + component.patientRevisit = true; + spyOn(component, 'dobChangeByCalender'); + fixture.detectChanges(); + }); + + it('loads beneficiary edit data once master and edit data exist', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({ benImage: 'IMG' })); + registrar.beneficiaryEditDetails$.next(revisit()); + registrar.registrationMasterDetails$.next(MASTER); + expect(component.revisitData.beneficiaryID).toBe('B1'); + expect(form.value.firstName).toBe('Ram'); + expect(form.value.phoneNo).toBe('9876543210'); + expect(form.value.parentRegID).toBe('70'); + expect(form.value.parentRelation).toBe('1'); + expect(form.value.benPhMapID).toBe('3'); + expect(form.value.benRelationshipType).toBe('Self'); + expect(form.value.gender).toBe(1); + expect(form.value.maritalStatus).toBe(2); + expect(form.value.maritalStatusName).toBe('Married'); + expect(form.value.income).toBe(2); + expect(form.value.incomeName).toBe('BPL'); + expect(form.value.educationQualification).toBe(2); + expect(form.value.occupationOther).toBe('Farmer'); + expect(form.value.image).toBe('IMG'); + expect(component.enableMarriageDetails).toBeTrue(); + expect(component._parentBenRegID).toBe('70'); + expect(component.dobChangeByCalender).toHaveBeenCalledWith(undefined); + expect(benDetails.getBeneficiaryImage).toHaveBeenCalledWith(77); + // male: widow(5) filtered out + expect( + component.maritalStatusMaster.map((m: any) => m.maritalStatusID) + ).toEqual([1, 2, 6, 7]); + }); + + it('ignores edit data without beneficiaryID', () => { + registrar.beneficiaryEditDetails$.next({ foo: 1 }); + registrar.registrationMasterDetails$.next(MASTER); + expect(component.revisitData).toBeUndefined(); + }); + + it('ngOnDestroy unsubscribes both subscriptions', () => { + registrar.registrationMasterDetails$.next(MASTER); + const sub = component.revisitDataSubscription; + component.ngOnDestroy(); + expect(sub.closed).toBeTrue(); + expect(component.masterDataSubscription.closed).toBeTrue(); + }); }); - it('should load the personal Details Form', () => { - component.ngOnInit(); - expect(component.personalDetailsForm).toBeTruthy(); + describe('pushEditingDatatoForm', () => { + beforeEach(() => { + fixture.detectChanges(); + component.masterData = MASTER; + }); + + it('clears marriage details for unmarried beneficiaries and handles empty phone maps', () => { + spyOn(component, 'dobChangeByCalender'); + component.pushEditingDatatoForm( + revisit({ + benPhoneMaps: [], + maritalStatus: { maritalStatusID: 1, status: 'Unmarried' }, + spouseName: 'X', + ageAtMarriage: 18, + i_bendemographics: {}, + literacyStatus: null, + }) + ); + expect(form.value.phoneNo).toBeNull(); + expect(form.value.parentRegID).toBe('null'); + expect(form.value.benRelationshipType).toBe('null'); + expect(form.value.income).toBeNull(); + expect(form.value.educationQualification).toBeNull(); + expect(form.value.occupation).toBeNull(); + expect(component.enableMarriageDetails).toBeFalse(); + expect(form.value.spouseName).toBeNull(); + expect(form.value.ageAtMarriage).toBeNull(); + expect(component._parentBenRegID).toBe('null'); + }); + + it('status 7 also disables marriage details; missing marital status name becomes "null"', () => { + spyOn(component, 'dobChangeByCalender'); + component.pushEditingDatatoForm( + revisit({ maritalStatus: { maritalStatusID: 7 } }) + ); + expect(component.enableMarriageDetails).toBeFalse(); + expect(form.value.maritalStatusName).toBe('null'); + }); + + it('currently throws because dobChangeByCalender(undefined) dereferences undefined', () => { + // documents app bug: (dobval || dobval.length === 10) with dobval undefined + expect(() => component.pushEditingDatatoForm(revisit())).toThrowError( + TypeError + ); + }); }); - it('should give error as Firstname is supposed to be more than 2 characters', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ firstName: 'A' }); - fixture.detectChanges(); - const firstName = component.personalDetailsForm.controls['firstName']; - expect(firstName.valid).toBeFalsy(); + describe('validateMaritalStatusMaster', () => { + beforeEach(() => (component.masterData = MASTER)); + it('transgender gets full list', () => { + component.validateMaritalStatusMaster({ m_gender: { genderID: 3 } }); + expect(component.maritalStatusMaster).toBe(MARITAL); + }); + it('female excludes widower(6)', () => { + component.validateMaritalStatusMaster({ m_gender: { genderID: 2 } }); + expect( + component.maritalStatusMaster.map((m: any) => m.maritalStatusID) + ).toEqual([1, 2, 5, 7]); + }); + it('unknown gender gives empty list', () => { + component.validateMaritalStatusMaster({ m_gender: { genderID: 9 } }); + expect(component.maritalStatusMaster).toEqual([]); + }); }); - it('should show as valid Firstname is supposed to be more than 2 characters and we have given 3 characters', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ firstName: 'ABC' }); - fixture.detectChanges(); - const firstName = component.personalDetailsForm.controls['firstName']; - expect(firstName.valid).toBeTruthy(); + describe('getBenImage', () => { + beforeEach(() => (component.revisitData = { beneficiaryRegID: 5 })); + it('does not patch without image', () => { + benDetails.getBeneficiaryImage.and.returnValue(of({})); + component.getBenImage(); + expect(form.value.image).toBeNull(); + }); + it('handles null response', () => { + benDetails.getBeneficiaryImage.and.returnValue(of(null)); + component.getBenImage(); + expect(form.value.image).toBeNull(); + }); }); - it('should set Contact Number as Required by Default', () => { - component.ngOnInit(); - expect(component.isMobileNoRequired).toBeTruthy(); + it('getPhoneMaps returns first phone or null', () => { + expect(component.getPhoneMaps([{ phoneNo: '1' }])).toBe('1'); + expect(component.getPhoneMaps([])).toBeNull(); + expect(component.getPhoneMaps(null)).toBeNull(); }); - it('should show PhoneNo as invalid as we have given string', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ phoneNo: 'ABC' }); - const phoneNo = component.personalDetailsForm.controls['phoneNo']; - fixture.detectChanges(); - expect(phoneNo.status).toBe('INVALID'); + describe('captureImage', () => { + it('patches image in new mode without change flag', () => { + component.captureImage(); + expect(form.value.image).toBe('img'); + expect(form.value.imageChangeFlag).toBeFalse(); + }); + it('sets change flag in revisit mode', () => { + component.patientRevisit = true; + component.captureImage(); + expect(form.value.imageChangeFlag).toBeTrue(); + expect(form.value.image).toBe('img'); + }); + it('does nothing when capture is cancelled', () => { + camera.capture.and.returnValue(of(null)); + component.captureImage(); + expect(form.value.image).toBeNull(); + }); }); - it('should show PhoneNo as valid as we have given chars as required', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ phoneNo: 9876543210 }); - fixture.detectChanges(); - const phoneNo = component.personalDetailsForm.controls['phoneNo']; - expect(phoneNo.valid).toBeTruthy(); + it('checkbox handlers toggle the required flags', () => { + component.checkMobileNoIsRequired({ checked: false }); + expect(component.isMobileNoRequired).toBeFalse(); + component.checkMobileNoIsRequired({ checked: true }); + expect(component.isMobileNoRequired).toBeTrue(); + component.checkOccuptionsRequired({ checked: false }); + expect(component.isOccuptionRequired).toBeFalse(); + component.checkOccuptionsRequired({ checked: true }); + expect(component.isOccuptionRequired).toBeTrue(); + component.checkFingerPrintIsRequired({ checked: false }); + expect(component.isFingerPrintRequired).toBeFalse(); + component.checkFingerPrintIsRequired({ checked: true }); + expect(component.isFingerPrintRequired).toBeTrue(); }); - it("should by default set the Age Unit to 'Years' ", () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - expect(form.value.ageUnit).toBe('Years'); + describe('onGenderSelected', () => { + beforeEach(() => (component.masterData = MASTER)); + + it('male sets gender name and filters widow', () => { + form.patchValue({ gender: 1 }); + component.onGenderSelected(); + expect(form.value.genderName).toBe('Male'); + expect(component.maritalStatusMaster.length).toBe(4); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('transgender confirmed gets full marital list', () => { + form.patchValue({ gender: 3 }); + component.onGenderSelected(); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + 'You have selected Transgender, please confirm' + ); + expect(component.maritalStatusMaster).toBe(MARITAL); + expect(form.value.genderName).toBe('Transgender'); + }); + + it('transgender declined clears gender', () => { + confirmation.confirm.and.returnValue(of(false)); + form.patchValue({ gender: 3 }); + component.onGenderSelected(); + expect(form.value.gender).toBeNull(); + expect(form.value.genderName).toBeNull(); + }); + + it('transgender confirm error is swallowed', () => { + confirmation.confirm.and.returnValue(throwingObs()); + form.patchValue({ gender: 3 }); + component.onGenderSelected(); + expect(form.value.gender).toBe(3); + }); }); - it('should call Camera Service to Capture Image', () => { - spyOn(cameraService, 'capture').and.callFake(function () { - return Observable.of(1); + describe('changeLiteracyStatus', () => { + it('resets education qualification when Literate', () => { + form.patchValue({ + literacyStatus: 'Literate', + educationQualification: 2, + }); + component.changeLiteracyStatus(); + expect(form.value.educationQualification).toBeNull(); + }); + it('leaves it for Illiterate', () => { + form.patchValue({ + literacyStatus: 'Illiterate', + educationQualification: 2, + }); + component.changeLiteracyStatus(); + expect(form.value.educationQualification).toBe(2); }); - component.ngOnInit(); - component.captureImage(); - const form = component.personalDetailsForm; - fixture.detectChanges(); - expect(cameraService.capture).toHaveBeenCalled(); }); - it('should set ageLimit for beneficiary to be 120 years', () => { - component.ngOnInit(); - const limit = component.ageLimit; - fixture.detectChanges(); - expect(limit).toBe(120); + describe('getParentDetails', () => { + it('sets parent from search results', () => { + registrar.identityQuickSearch.and.returnValue( + of([{ benPhoneMaps: [{ parentBenRegID: 42 }] }]) + ); + form.patchValue({ phoneNo: '9876543210' }); + component.getParentDetails(); + expect(registrar.identityQuickSearch).toHaveBeenCalledWith({ + beneficiaryRegID: null, + beneficiaryID: null, + phoneNo: '9876543210', + }); + expect(form.value.parentRegID).toBe(42); + expect(form.value.parentRelation).toBe(11); + }); + + it('sets self relation when no results (new mode)', () => { + registrar.identityQuickSearch.and.returnValue(of([])); + form.patchValue({ phoneNo: '9876543210' }); + component.getParentDetails(); + expect(form.value.parentRegID).toBeNull(); + expect(form.value.parentRelation).toBe(1); + }); + + it('uses own regID as parent when no results in revisit mode', () => { + component.patientRevisit = true; + registrar.identityQuickSearch.and.returnValue(of([{ benPhoneMaps: [] }])); + form.patchValue({ phoneNo: '9876543210', beneficiaryRegID: 88 }); + component.getParentDetails(); + expect(form.value.parentRegID).toBe(88); + expect(form.value.parentRelation).toBe(1); + }); + + it('alerts and clears on search error', () => { + registrar.identityQuickSearch.and.returnValue(throwingObs('err')); + form.patchValue({ phoneNo: '9876543210' }); + component.getParentDetails(); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + expect(form.value.phoneNo).toBeNull(); + expect(form.value.parentRelation).toBe(1); + }); + + it('clears when phone number is not 10 digits (new mode)', () => { + form.patchValue({ phoneNo: '123', parentRegID: 5, parentRelation: 1 }); + component.getParentDetails(); + expect(registrar.identityQuickSearch).not.toHaveBeenCalled(); + expect(form.value.parentRegID).toBeNull(); + expect(form.value.parentRelation).toBeNull(); + expect(form.value.phoneNo).toBeNull(); + }); + + it('restores saved parent when phone invalid in revisit mode', () => { + component.patientRevisit = true; + component._parentBenRegID = '70'; + form.patchValue({ phoneNo: null }); + component.getParentDetails(); + expect(form.value.parentRegID).toBe('70'); + expect(form.value.parentRelation).toBeNull(); + }); }); - it('should set Minimum AgeLimit for Marriage 12 years', () => { - component.ngOnInit(); - const limit = component.ageforMarriage; - fixture.detectChanges(); - expect(limit).toBe(12); + describe('age handling', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('onAgeEntered rejects ages over the limit', () => { + form.patchValue({ age: 150, ageUnit: 'Years' }); + component.onAgeEntered(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.ageRestriction, + 'info' + ); + expect(form.value.age).toBeNull(); + }); + + it('onAgeEntered computes dob and enables marital status for adults', () => { + form.patchValue({ age: 30, ageUnit: 'Years' }); + component.onAgeEntered(); + const years = moment().diff(moment(form.value.dob), 'years'); + expect(years).toBe(30); + expect(component.enableMaritalStatus).toBeTrue(); + }); + + it('onAgeEntered with child age disables and clears marital status', () => { + form.patchValue({ + age: 5, + ageUnit: 'Years', + maritalStatus: 2, + maritalStatusName: 'Married', + spouseName: 'S', + }); + component.onAgeEntered(); + expect(component.enableMaritalStatus).toBeFalse(); + expect(form.value.maritalStatus).toBeNull(); + expect(form.value.spouseName).toBeNull(); + expect(component.enableMarriageDetails).toBeFalse(); + }); + + it('onAgeEntered with no age only re-evaluates eligibility', () => { + form.patchValue({ age: null, dob: null }); + component.onAgeEntered(); + expect(form.value.dob).toBeNull(); + expect(component.enableMaritalStatus).toBeFalse(); + }); + + it('onAgeUnitEntered only recalculates when age is set', () => { + spyOn(component, 'onAgeEntered'); + form.patchValue({ age: null }); + component.onAgeUnitEntered(); + expect(component.onAgeEntered).not.toHaveBeenCalled(); + form.patchValue({ age: 3 }); + component.onAgeUnitEntered(); + expect(component.onAgeEntered).toHaveBeenCalled(); + }); }); - it('should call for Confirmation if Gender is selected as Type 3, Transgender', () => { - spy = spyOn(confirmationService, 'confirm').and.callFake(function () { - return Observable.of(1); - }); - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ gender: 3 }); - fixture.detectChanges(); - component.onGenderSelected(); - expect(spy).toHaveBeenCalledWith( - 'Gender Confirmation', - 'You have selected Transgender, Please Confirm.' - ); + describe('dobChangeByCalender', () => { + beforeEach(() => fixture.detectChanges()); + + it('sets age in years', () => { + component.dateForCalendar = moment() + .subtract(25, 'years') + .subtract(10, 'days') + .toDate(); + component.dobChangeByCalender('x'); + expect(form.value.age).toBe(25); + expect(form.value.ageUnit).toBe('Years'); + expect(component.enableMaritalStatus).toBeTrue(); + }); + + it('sets age in months', () => { + component.dateForCalendar = moment() + .subtract(3, 'months') + .subtract(5, 'days') + .toDate(); + component.dobChangeByCalender('x'); + expect(form.value.ageUnit).toBe('Months'); + expect(form.value.age).toBeGreaterThan(0); + }); + + it('sets age in days', () => { + component.dateForCalendar = moment().subtract(5, 'days').toDate(); + component.dobChangeByCalender('x'); + expect(form.value.ageUnit).toBe('Days'); + expect(form.value.age).toBeGreaterThan(0); + }); + + it('today is 1 day', () => { + component.dateForCalendar = new Date(); + component.dobChangeByCalender('x'); + expect(form.value.age).toBe(1); + expect(form.value.ageUnit).toBe('Days'); + }); + + it('invalid date string clears dob and alerts', () => { + component.dateForCalendar = null; + form.patchValue({ dob: new Date() }); + component.dobChangeByCalender('Invalid date'); + expect(form.value.dob).toBeNull(); + expect(component.dateForCalendar).toBeNull(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.invalidData, + 'info' + ); + }); + + it('otherwise clears age', () => { + component.dateForCalendar = null; + form.patchValue({ age: 5 }); + component.dobChangeByCalender('x'); + expect(form.value.age).toBeNull(); + }); }); - it('should enable Marital Status if Age entered is greater than 12', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ age: 13 }); - fixture.detectChanges(); - component.onAgeEntered(); + describe('clearMaritalStatus', () => { + it('does nothing when already null', () => { + component.enableMarriageDetails = true; + component.clearMaritalStatus(); + expect(component.enableMarriageDetails).toBeTrue(); + }); + }); - expect(component.enableMaritalStatus).toBe(true); + it('onIncomeChanged sets income name', () => { + component.masterData = MASTER; + form.patchValue({ income: 1 }); + component.onIncomeChanged(); + expect(form.value.incomeName).toBe('APL'); }); - it('should disable Marital Status if Age entered is lesser than 12', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ age: 11 }); - fixture.detectChanges(); - component.onAgeEntered(); + describe('onMaritalStatusChanged', () => { + beforeEach(() => (component.masterData = MASTER)); + + it('married enables marriage details and spouse mandatory', () => { + form.patchValue({ maritalStatus: 2, spouseName: 'S', ageAtMarriage: 20 }); + component.onMaritalStatusChanged(); + expect(component.enableMarriageDetails).toBeTrue(); + expect(component.enableSpouseMandatory).toBeTrue(); + expect(form.value.maritalStatusName).toBe('Married'); + expect(form.value.spouseName).toBeNull(); + expect(form.value.ageAtMarriage).toBeNull(); + }); + + it('unmarried disables marriage details', () => { + form.patchValue({ maritalStatus: 1 }); + component.onMaritalStatusChanged(); + expect(component.enableMarriageDetails).toBeFalse(); + expect(component.enableSpouseMandatory).toBeFalse(); + expect(form.value.maritalStatusName).toBe('Unmarried'); + }); - expect(component.enableMaritalStatus).toBe(false); + it('status 7 disables marriage details', () => { + form.patchValue({ maritalStatus: 7 }); + component.onMaritalStatusChanged(); + expect(component.enableMarriageDetails).toBeFalse(); + }); + + it('widow enables marriage details without spouse mandatory', () => { + form.patchValue({ maritalStatus: 5 }); + component.onMaritalStatusChanged(); + expect(component.enableMarriageDetails).toBeTrue(); + expect(component.enableSpouseMandatory).toBeFalse(); + }); }); - it('should disable marriage details if Beneficiary is UnMarried', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ age: 15, maritalStatus: 1 }); - fixture.detectChanges(); - component.onMaritalStatusChanged(); + describe('checkAgeAtMarriage', () => { + beforeEach(() => fixture.detectChanges()); + const info = () => LANGUAGE_EN.alerts.info; + const ageMsg = () => `${info().marriageAge} 12 ${info().years}`; + + it('no-op when ageAtMarriage null', () => { + component.checkAgeAtMarriage(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('requires age first', () => { + form.patchValue({ ageAtMarriage: 20, age: null }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).toHaveBeenCalled(); + expect(form.value.ageAtMarriage).toBeNull(); + }); - expect(component.enableMarriageDetails).toBe(false); + it('requires Years unit', () => { + form.patchValue({ ageAtMarriage: 20, age: 30, ageUnit: 'Months' }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).toHaveBeenCalledWith(ageMsg(), 'info'); + expect(form.value.ageAtMarriage).toBeNull(); + }); + + it('rejects when age below marriage age', () => { + form.patchValue({ ageAtMarriage: 20, age: 10, ageUnit: 'Years' }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).toHaveBeenCalledWith(ageMsg(), 'info'); + }); + + it('rejects ageAtMarriage below 12', () => { + form.patchValue({ ageAtMarriage: 10, age: 30, ageUnit: 'Years' }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).toHaveBeenCalledWith(ageMsg(), 'info'); + expect(form.value.ageAtMarriage).toBeNull(); + }); + + it('rejects ageAtMarriage greater than age', () => { + form.patchValue({ ageAtMarriage: 40, age: 30, ageUnit: 'Years' }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.common.marriageatageismorethantheactualage, + 'info' + ); + expect(form.value.ageAtMarriage).toBeNull(); + }); + + it('accepts a valid ageAtMarriage', () => { + form.patchValue({ ageAtMarriage: 20, age: 30, ageUnit: 'Years' }); + component.checkAgeAtMarriage(); + expect(confirmation.alert).not.toHaveBeenCalled(); + expect(form.value.ageAtMarriage).toBe(20); + }); }); - it("should disable marriage details if Beneficiary Marriage status is 'Not Applicable' ", () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ age: 15, maritalStatus: 7 }); - fixture.detectChanges(); - component.onMaritalStatusChanged(); + describe('getOccupationName', () => { + beforeEach(() => (component.masterData = MASTER)); + it('sets occupation name for known occupation', () => { + form.patchValue({ occupation: 1 }); + component.getOccupationName(); + expect(form.value.occupationOther).toBe('Farmer'); + }); + it('clears for "Other" (7)', () => { + form.patchValue({ occupation: 7, occupationOther: 'typed' }); + component.getOccupationName(); + expect(form.value.occupationOther).toBeNull(); + }); + }); - expect(component.enableMarriageDetails).toBe(false); + it('onEducationQualificationChanged sets name', () => { + component.masterData = MASTER; + form.patchValue({ educationQualification: 1 }); + component.onEducationQualificationChanged(); + expect(form.value.educationQualificationName).toBe('Primary'); }); - it('should enable marriage details if Beneficiary Marriage status is neither Unmarried or Nor Not Applicable ', () => { - const randomNumber = Math.floor(25 * (6 - 2 + 1)) + 2; - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ age: 15, maritalStatus: randomNumber }); - fixture.detectChanges(); - component.onMaritalStatusChanged(); + it('onScrollEvent hides the datepicker', () => { + const hide = jasmine.createSpy('hide'); + component.datepicker = { hide } as any; + component.onScrollEvent(); + expect(hide).toHaveBeenCalled(); + }); - expect(component.enableMarriageDetails).toBe(true); + it('AfterViewChecked runs change detection', () => { + const cdr = (component as any).changeDetectorRef; + spyOn(cdr, 'detectChanges'); + component.AfterViewChecked(); + expect(cdr.detectChanges).toHaveBeenCalled(); }); - it('should show as invalid aadhaarno is supposed to be numbers and we have given characters', () => { - component.ngOnInit(); - const form = component.personalDetailsForm; - form.patchValue({ aadharNo: 'ABCB' }); - fixture.detectChanges(); - const aadhar = component.personalDetailsForm.controls['aadharNo']; - expect(aadhar.status).toBe('INVALID'); + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('firstName'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'firstName', + 'Register Personal Details' + ); }); }); diff --git a/src/app/app-modules/registrar/registration/registration.component.spec.ts b/src/app/app-modules/registrar/registration/registration.component.spec.ts index 8b5864e0..04c30d00 100644 --- a/src/app/app-modules/registrar/registration/registration.component.spec.ts +++ b/src/app/app-modules/registrar/registration/registration.component.spec.ts @@ -21,218 +21,751 @@ */ import { - async, ComponentFixture, - tick, - inject, - fakeAsync, TestBed, + fakeAsync, + flush, } from '@angular/core/testing'; -import { RegistrationComponent } from './registration.component'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { FormArray, FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; import { ConfirmationService } from '../../core/services/confirmation.service'; -import { CameraService } from '../../core/services/camera.service'; -import { RegistrarService } from '../../../../../Common-UI/src/registrar/services/registrar.service'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -// import { Location } from '@angular/common'; -// import { fakeAsync, tick } from '@angular/core/testing'; -// import { RouterTestingModule } from '@angular/router/testing'; -import { Router, ActivatedRoute } from '@angular/router'; -import { MaterialModule } from '../../core/material.module'; - -class RouterStub { - navigateByUrl(url: string) { - return url; - } -} -class MockActivatedRoute { - snapshot = { - params: [null], - }; -} - -class RegistrarServiceMock { - // submitBeneficiary = jasmine.createSpy('submitBeneficiary').andCallFake(function () { - // return Observable.of(true); - // }); - // updateBeneficiary = jasmine.createSpy('updateBeneficiary').andCallFake(function () { - // return Observable.of(true); - // }); - - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, - { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', - }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, - ], - }; - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } +import { SetLanguageComponent } from '../../core/components/set-language.component'; +import { RegistrarService } from '../shared/services/registrar.service'; +import { RegistrationComponent } from './registration.component'; - submitBeneficiary() { - return Observable.of(true); - } - updateBeneficiary() { - return Observable.of(false); - } -} +const GOV_MASTER = [ + { govtIdentityTypeID: 1, identityType: 'Aadhar' }, + { govtIdentityTypeID: 1, identityType: 'Aadhar-dup' }, + { govtIdentityTypeID: 3, identityType: 'PAN' }, +]; +const OTHER_GOV_MASTER = [ + { govtIdentityTypeID: 7, identityType: 'Ration' }, + { govtIdentityTypeID: 7, identityType: 'Ration-dup' }, +]; +const MASTER = { + govIdEntityMaster: GOV_MASTER, + otherGovIdEntityMaster: OTHER_GOV_MASTER, + other: 'x', +}; -describe('RegisterComponent', () => { +describe('RegistrationComponent', () => { let component: RegistrationComponent; let fixture: ComponentFixture; - let registrarService: RegistrarService; - let el: HTMLElement; - let spy: any; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - MaterialModule, - NoopAnimationsModule, - ], + let registrar: any; + let confirmation: any; + let router: Router; + let route: any; + + const otherDetailsStub = { + resetForm: jasmine.createSpy('resetForm'), + getRemovedIDs: jasmine.createSpy('getRemovedIDs').and.returnValue({ + removedGovIDs: [], + removedOtherGovIDs: [], + }), + }; + const demographicStub = { + setDemographicDefaults: jasmine.createSpy('setDemographicDefaults'), + }; + const personalStub: any = {}; + + async function setup(params: any = {}) { + route = { snapshot: { params } }; + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(MASTER), + beneficiaryEditDetails$: new BehaviorSubject(null), + }); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [RegistrationComponent], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - CameraService, - ConfirmationService, - { provide: Router, useClass: RouterStub }, - { provide: ActivatedRoute, useClass: MockActivatedRoute }, + ...commonTestProviders({ + session: { + serviceLineDetails: JSON.stringify({ + vanID: 11, + parkingPlaceID: 22, + }), + userName: 'reg', + servicePointID: 5, + servicePointName: 'SP', + providerServiceID: 99, + }, + }), + { provide: RegistrarService, useValue: registrar }, + { provide: ActivatedRoute, useValue: route }, + { provide: SetLanguageComponent, useValue: {} }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { fixture = TestBed.createComponent(RegistrationComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - // component.ngOnInit(); + confirmation = TestBed.inject(ConfirmationService) as any; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + fixture.detectChanges(); + (component as any).otherDetails = otherDetailsStub; + (component as any).demographicDetails = demographicStub; + (component as any).personalDetails = personalStub; + } - spyOn(component, 'dateFormatChange').and.returnValue(1); + const personal = () => component.personalDetailsForm; + const demo = () => component.demographicDetailsForm; + const other = () => component.otherDetailsForm; + const invalidate = (form: FormGroup, names: string[]) => + names.forEach(n => form.controls[n].setErrors({ required: true })); - fixture.detectChanges(); + afterEach(() => { + otherDetailsStub.resetForm.calls.reset(); + demographicStub.setDemographicDefaults.calls.reset(); + fixture.destroy(); }); - it('should create Registration Component', () => { - expect(component).toBeTruthy(); - }); + describe('new registration mode', () => { + beforeEach(async () => setup()); - it('should load form for new Beneficiary Registration', () => { - // component.ngOnInit(); - const form = component.beneficiaryRegistrationForm; - expect(form).toBeTruthy(); - }); + it('initialises forms, language, master data and mode', () => { + expect(component.beneficiaryRegistrationForm).toBeTruthy(); + expect(personal()).toBe( + component.beneficiaryRegistrationForm.get( + 'personalDetailsForm' + ) as FormGroup + ); + expect(demo()).toBeTruthy(); + expect(other()).toBeTruthy(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(registrar.getRegistrationMaster).toHaveBeenCalledWith(1); + expect(component.masterData).toEqual(MASTER); + expect(component.govIDMaster).toEqual(GOV_MASTER); + expect(component.otherGovIDMaster).toEqual(OTHER_GOV_MASTER); + expect(component.patientRevisit).toBeFalse(); + }); - it('should load component in New Beneficiary Mode ', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(component.patientRevisit).toBe(false); - }); + it('renders reset + submit buttons but no update/cancel in new mode', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('#resetButton')).not.toBeNull(); + expect(el.querySelector('#submitButton')).not.toBeNull(); + expect(el.querySelector('#saveButton')).toBeNull(); + expect(el.querySelector('#cancelButton')).toBeNull(); + }); - it('should set Post button text to Submit', () => { - // component.ngOnInit(); - const buttonText = component.postButtonText; - expect(buttonText).toBe('Submit'); - }); + it('ignores null master data emissions', () => { + component.govIDMaster = 'keep'; + registrar.registrationMasterDetails$.next(null); + expect(component.govIDMaster).toBe('keep'); + }); + + it('setStep changes the accordion step', () => { + component.setStep(2); + expect(component.step).toBe(2); + }); + + it('ngOnDestroy does nothing when not revisit', () => { + component.ngOnDestroy(); + expect(registrar.clearBeneficiaryEditDetails).not.toHaveBeenCalled(); + }); + + it('ngDoCheck refreshes the language set', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + }); - it('should call the submit Service Method', () => { - const service = fixture.debugElement.injector.get(RegistrarService); - spy = spyOn(service, 'submitBeneficiary').and.callFake(function () { - return Observable.of(true); + it('ngAfterViewChecked triggers change detection', () => { + const cdr = (component as any).changeDetectorRef; + spyOn(cdr, 'detectChanges'); + component.ngAfterViewChecked(); + expect(cdr.detectChanges).toHaveBeenCalled(); }); - component.submitBeneficiaryDetails(); - expect(service.submitBeneficiary).toHaveBeenCalled(); + + describe('resetBeneficiaryForm', () => { + it('resets the form and child component state', () => { + const govID = other().controls['govID'] as FormArray; + govID.push(component.utils.initGovID()); + govID.push(component.utils.initGovID()); + const otherGovID = other().controls['otherGovID'] as FormArray; + otherGovID.push(component.utils.initGovID()); + personal().patchValue({ firstName: 'A' }); + component.step = 2; + component.resetBeneficiaryForm(); + expect(personal().value.firstName).toBeNull(); + expect(personal().value.ageUnit).toBe('Years'); + expect(personal().value.checked).toBeTrue(); + expect(govID.length).toBe(1); + expect(otherGovID.length).toBe(1); + expect(otherDetailsStub.resetForm).toHaveBeenCalled(); + expect(demographicStub.setDemographicDefaults).toHaveBeenCalled(); + expect(personalStub.enableMaritalStatus).toBeFalse(); + expect(personalStub.enableMarriageDetails).toBeFalse(); + expect(personalStub.isMobileNoRequired).toBeTrue(); + expect(personalStub.isOccuptionRequired).toBeTrue(); + expect(component.step).toBe(0); + }); + }); + + describe('govIDReset', () => { + it('handles empty arrays', () => { + (other().controls['govID'] as FormArray).clear(); + (other().controls['otherGovID'] as FormArray).clear(); + component.govIDReset(); + expect((other().controls['govID'] as FormArray).length).toBe(0); + expect((other().controls['otherGovID'] as FormArray).length).toBe(0); + }); + }); + + describe('confirmFormReset', () => { + it('pristine + reset=false navigates to search', () => { + component.confirmFormReset(false); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('pristine + reset=true does nothing', () => { + component.confirmFormReset(true); + expect(router.navigate).not.toHaveBeenCalled(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('dirty + reset=true resets after confirmation', () => { + component.beneficiaryRegistrationForm.markAsDirty(); + spyOn(component, 'resetBeneficiaryForm'); + component.confirmFormReset(true); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'warn', + 'Do you want to reset the entered details?' + ); + expect(component.resetBeneficiaryForm).toHaveBeenCalled(); + }); + + it('dirty + reset=true does not reset when declined', () => { + component.beneficiaryRegistrationForm.markAsDirty(); + confirmation.confirm.and.returnValue(of(false)); + spyOn(component, 'resetBeneficiaryForm'); + component.confirmFormReset(true); + expect(component.resetBeneficiaryForm).not.toHaveBeenCalled(); + }); + + it('dirty + reset=false navigates after confirmation', () => { + component.beneficiaryRegistrationForm.markAsDirty(); + component.confirmFormReset(false); + expect(confirmation.confirm).toHaveBeenCalledWith( + 'info', + LANGUAGE_EN.alerts.info.navigateFurtherAlert, + 'Yes', + 'No' + ); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + }); + + it('dirty + reset=false stays when declined', () => { + component.beneficiaryRegistrationForm.markAsDirty(); + confirmation.confirm.and.returnValue(of(false)); + component.confirmFormReset(false); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('dirty + other value does nothing', () => { + component.beneficiaryRegistrationForm.markAsDirty(); + component.confirmFormReset('x'); + expect(confirmation.confirm).not.toHaveBeenCalled(); + }); + + it('search button triggers confirmFormReset(false)', () => { + spyOn(component, 'confirmFormReset'); + ( + fixture.nativeElement.querySelector('#moveToSearch') as HTMLElement + ).click(); + expect(component.confirmFormReset).toHaveBeenCalledWith(false); + }); + }); + + describe('cancelBeneficiaryChanges', () => { + it('navigates on confirm', () => { + component.cancelBeneficiaryChanges(); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + }); + it('stays on decline', () => { + confirmation.confirm.and.returnValue(of(false)); + component.cancelBeneficiaryChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('checkValids', () => { + it('returns true for a valid form', () => { + demo().patchValue({ stateID: 1 }); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + expect(confirmation.notify).not.toHaveBeenCalled(); + }); + + it('collects all personal, demographic and other messages', () => { + const L = LANGUAGE_EN; + personal().patchValue({ + age: 20, + ageUnit: 'Years', + maritalStatus: 2, + literacyStatus: 'Literate', + }); + invalidate(personal(), [ + 'maritalStatus', + 'firstName', + 'gender', + 'phoneNo', + 'age', + 'ageAtMarriage', + 'spouseName', + 'occupationOther', + 'educationQualification', + 'lastName', + ]); + invalidate(demo(), [ + 'districtID', + 'blockID', + 'parkingPlace', + 'zoneID', + 'servicePoint', + 'pincode', + ]); + invalidate(other(), [ + 'emailID', + 'religionOther', + 'govID', + 'otherGovID', + 'fatherName', + 'community', + 'bankName', + ]); + const result = component.checkValids( + component.beneficiaryRegistrationForm + ); + expect(result).toBeFalse(); + const required = confirmation.notify.calls.mostRecent().args[1]; + expect(confirmation.notify.calls.mostRecent().args[0]).toBe( + L.alerts.info.mandatoryFields + ); + expect(required).toEqual( + jasmine.arrayContaining([ + L.ro.personalInfo.maritalStatus, + L.ro.personalInfo.firstName, + L.ro.personalInfo.gender, + L.bendetails.phoneNo, + L.bendetails.age, + L.ro.personalInfo.ageAtMarriage, + L.ro.personalInfo.spouseName, + L.ro.personalInfo.otherOccupation, + L.ro.personalInfo.educationalQualification, + 'lastName', + L.ro.locInfo.state, + L.ro.locInfo.districtTownCity, + L.ro.locInfo.taluk, + 'pincode', + L.emailAddress, + L.otherReligionName, + L.otherGovtID, + L.ro.otherInfo.fName, + L.ro.otherInfo.community, + 'bankName', + L.govID, + ]) + ); + const village = (demo().controls['villages'] as FormArray).at( + 0 + ) as FormGroup; + expect(village.controls['villageID'].disabled).toBeTrue(); + }); + + it('skips age-dependent messages for children / non-years units', () => { + personal().patchValue({ age: 5, ageUnit: 'Months', maritalStatus: 1 }); + demo().patchValue({ stateID: 1 }); + invalidate(personal(), [ + 'maritalStatus', + 'ageAtMarriage', + 'spouseName', + 'educationQualification', + ]); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + }); + + it('skips ageAtMarriage for unmarried(1)/7 statuses and spouse for non-2', () => { + personal().patchValue({ age: 30, ageUnit: 'Years', maritalStatus: 7 }); + demo().patchValue({ stateID: 1 }); + invalidate(personal(), ['ageAtMarriage', 'spouseName']); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + personal().patchValue({ maritalStatus: 1 }); + invalidate(personal(), ['ageAtMarriage']); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + }); + + it('flags govIDs shorter than max/min length', () => { + demo().patchValue({ stateID: 1 }); + const govID = other().controls['govID'] as FormArray; + govID.at(0).patchValue({ type: 1, idValue: '12', maxLength: 12 }); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeFalse(); + expect(confirmation.notify.calls.mostRecent().args[1]).toEqual([ + LANGUAGE_EN.govID, + ]); + govID.at(0).patchValue({ type: 3, idValue: '12', minLength: 10 }); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeFalse(); + govID + .at(0) + .patchValue({ type: 3, idValue: '1234567890', minLength: 10 }); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + govID + .at(0) + .patchValue({ type: 1, idValue: '123456789012', maxLength: 12 }); + expect( + component.checkValids(component.beneficiaryRegistrationForm) + ).toBeTrue(); + }); + }); + + describe('postButtonCall', () => { + it('does nothing when invalid', () => { + spyOn(component, 'submitBeneficiaryDetails'); + spyOn(component, 'updateBeneficiarynPassToNurse'); + component.postButtonCall(); + expect(component.submitBeneficiaryDetails).not.toHaveBeenCalled(); + expect(component.updateBeneficiarynPassToNurse).not.toHaveBeenCalled(); + }); + + it('submits in new mode', () => { + spyOn(component, 'checkValids').and.returnValue(true); + spyOn(component, 'submitBeneficiaryDetails'); + component.postButtonCall(); + expect(component.submitBeneficiaryDetails).toHaveBeenCalled(); + }); + + it('updates in revisit mode', () => { + component.patientRevisit = true; + spyOn(component, 'checkValids').and.returnValue(true); + spyOn(component, 'updateBeneficiarynPassToNurse'); + component.postButtonCall(); + expect(component.updateBeneficiarynPassToNurse).toHaveBeenCalled(); + }); + }); + + function fillForm() { + personal().patchValue({ + firstName: 'Ram', + lastName: 'K', + gender: 1, + genderName: 'Male', + dob: '2000-01-01T00:00:00.000Z', + phoneNo: '9999999999', + parentRegID: 3, + parentRelation: 1, + beneficiaryRegID: 55, + benPhMapID: 'null', + }); + demo().patchValue({ stateID: 1, stateName: 'S', districtID: 2 }); + (demo().controls['villages'] as FormArray) + .at(0) + .patchValue({ villageID: { districtBranchID: 9, villageName: 'V' } }); + (other().controls['govID'] as FormArray) + .at(0) + .patchValue({ type: 1, idValue: '1111' }); + (other().controls['otherGovID'] as FormArray) + .at(0) + .patchValue({ type: 7, idValue: 'R1' }); + } + + describe('submitBeneficiaryDetails', () => { + it('submits the iEMR payload and resets on success', () => { + fillForm(); + registrar.submitBeneficiary.and.returnValue( + of({ statusCode: 200, data: { response: 'Saved' } }) + ); + spyOn(component, 'resetBeneficiaryForm'); + component.submitBeneficiaryDetails(); + const payload = registrar.submitBeneficiary.calls.mostRecent().args[0]; + expect(payload.firstName).toBe('Ram'); + expect(payload.dOB).toBe('2000-01-01T00:00:00.000Z'); + expect(payload.vanID).toBe(11); + expect(payload.parkingPlaceID).toBe(22); + expect(payload.createdBy).toBe('reg'); + expect(payload.providerServiceMapID).toBe(99); + expect(payload.i_bendemographics.districtBranchID).toBe(9); + expect(payload.i_bendemographics.districtBranchName).toBe('V'); + expect(payload.i_bendemographics.servicePointID).toBe(5); + expect(payload.i_bendemographics.countryName).toBe('India'); + expect(payload.benPhoneMaps[0]).toEqual( + jasmine.objectContaining({ + phoneNo: '9999999999', + phoneTypeID: 1, + vanID: 11, + parkingPlaceID: 22, + createdBy: 'reg', + }) + ); + // duplicate master entries produce two identities for the same type + expect(payload.beneficiaryIdentities.length).toBe(4); + expect(payload.beneficiaryIdentities[0]).toEqual( + jasmine.objectContaining({ + govtIdentityNo: '1111', + govtIdentityTypeName: 'Aadhar', + identityType: 'National ID', + }) + ); + expect(payload.beneficiaryIdentities[2]).toEqual( + jasmine.objectContaining({ + govtIdentityNo: 'R1', + identityType: 'State ID', + }) + ); + expect(confirmation.alert).toHaveBeenCalledWith('Saved', 'success'); + expect(component.resetBeneficiaryForm).toHaveBeenCalled(); + }); + + it('alerts on non-200 response', () => { + fillForm(); + registrar.submitBeneficiary.and.returnValue(of({ statusCode: 5000 })); + component.submitBeneficiaryDetails(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issueInSavngData, + 'error' + ); + }); + }); + + it('makePhoneTypeID returns 1 or null', () => { + expect(component.makePhoneTypeID('123')).toBe(1); + expect(component.makePhoneTypeID('')).toBeNull(); + }); + + it('iEMRids skips entries without type/value or unmatched types', () => { + const res = component.iEMRids( + [ + { type: null, idValue: 'x' }, + { type: 99, idValue: 'y' }, + ], + [ + { type: 7, idValue: null }, + { type: 55, idValue: 'z' }, + ] + ); + expect(res).toEqual([]); + }); + + it('getBenPhMapID maps the string "null" to null', () => { + expect(component.getBenPhMapID('null')).toBeNull(); + expect(component.getBenPhMapID(4)).toBe(4); + }); + + it('getRelationTypeForUpdate maps relation ids', () => { + expect(component.getRelationTypeForUpdate(1, null)).toBe('Self'); + expect(component.getRelationTypeForUpdate(11, null)).toBe('Other'); + expect(component.getRelationTypeForUpdate(5, null)).toBeNull(); + }); + + it('dateFormatChange returns ISO string of dob', () => { + personal().patchValue({ dob: '2010-05-05T00:00:00.000Z' }); + expect(component.dateFormatChange()).toBe('2010-05-05T00:00:00.000Z'); + }); + + describe('iEMRidsUpdate', () => { + it('builds existing, removed and new identities with de-duplicated master', () => { + const res = component.iEMRidsUpdate( + [ + { type: 1, idValue: 'A', deleted: false, benIdentityId: 10 }, + { type: 3, idValue: 'N', deleted: null, benIdentityId: null }, + { type: null, idValue: 'skip' }, + ], + [ + { type: 7, idValue: 'O', deleted: false, benIdentityId: 20 }, + { type: 7, idValue: 'ON', deleted: false, benIdentityId: null }, + { type: 8, idValue: 'X', deleted: false, benIdentityId: 21 }, + { type: 8, idValue: 'XN', deleted: false, benIdentityId: null }, + ], + [{ type: 3, idValue: 'R', benIdentityId: 30, createdBy: 'old' }], + [ + { type: 7, idValue: 'RO', benIdentityId: 40, createdBy: 'old2' }, + { type: 9, idValue: 'nomatch' }, + ] + ); + expect(res.length).toBe(6); + expect(res[0]).toEqual( + jasmine.objectContaining({ + govtIdentityNo: 'A', + benIdentityId: 10, + deleted: false, + createdBy: 'reg', + }) + ); + expect(res[0].govtIdentityType.isGovtID).toBeTrue(); + expect(res[1].govtIdentityNo).toBe('O'); + expect(res[1].govtIdentityType.isGovtID).toBeFalse(); + expect(res[2]).toEqual( + jasmine.objectContaining({ + govtIdentityNo: 'R', + deleted: true, + createdBy: 'old', + }) + ); + expect(res[3]).toEqual( + jasmine.objectContaining({ govtIdentityNo: 'RO', deleted: true }) + ); + expect(res[3].govtIdentityType.isGovtID).toBeFalse(); + expect(res[4]).toEqual( + jasmine.objectContaining({ + govtIdentityNo: 'N', + deleted: false, + benIdentityId: undefined, + }) + ); + expect(res[5].govtIdentityNo).toBe('ON'); + }); + + it('returns undefined when nothing to send', () => { + expect(component.iEMRidsUpdate([], [], [], [])).toBeUndefined(); + }); + }); + + describe('update flows', () => { + beforeEach(() => fillForm()); + + it('updateBenDataManipulation builds the update payload', () => { + personal().patchValue({ + educationQualification: 2, + occupation: 3, + income: 4, + parentRelation: 11, + benPhMapID: 8, + }); + other().patchValue({ community: 5, religion: 6, emailID: 'a@b' }); + const form = component.updateBenDataManipulation(); + expect(otherDetailsStub.getRemovedIDs).toHaveBeenCalled(); + expect(form.passToNurse).toBeUndefined(); + expect(form.vanID).toBe(11); + expect(form.benPhoneMaps[0].modifiedBy).toBe('reg'); + expect(form.benPhoneMaps[0].benPhMapID).toBe(8); + expect( + form.benPhoneMaps[0].benRelationshipType.benRelationshipType + ).toBe('Other'); + expect(form.i_bendemographics.educationID).toBe(2); + expect(form.i_bendemographics.communityID).toBe(5); + expect(form.i_bendemographics.districtBranchID).toBe(9); + expect(form.email).toBe('a@b'); + expect(form.lastName).toBe('K'); + expect(form.dOB).toBe('2000-01-01T00:00:00.000Z'); + expect(form.beneficiaryIdentities.length).toBe(2); + }); + + it('iEMRFormUpdate maps empty values to undefined', () => { + personal().patchValue({ lastName: '', benPhMapID: 'null' }); + const form: any = component.iEMRFormUpdate(); + expect(form.lastName).toBeUndefined(); + expect(form.i_bendemographics.educationID).toBeUndefined(); + expect(form.benPhoneMaps[0].benPhMapID).toBeNull(); + expect( + form.benPhoneMaps[0].benRelationshipType.benRelationshipType + ).toBe('Self'); + }); + + it('updateBeneficiarynPassToNurse success navigates', () => { + registrar.updateBeneficiary.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.updateBeneficiarynPassToNurse(); + expect( + registrar.updateBeneficiary.calls.mostRecent().args[0].passToNurse + ).toBeTrue(); + expect(confirmation.alert).toHaveBeenCalledWith('Updated', 'success'); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + }); + + it('updateBeneficiarynPassToNurse failure alerts error', () => { + registrar.updateBeneficiary.and.returnValue( + of({ statusCode: 5000, errorMessage: 'bad' }) + ); + component.updateBeneficiarynPassToNurse(false); + expect( + registrar.updateBeneficiary.calls.mostRecent().args[0].passToNurse + ).toBeFalse(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('updateBeneficiaryDetails success does not pass to nurse', () => { + registrar.updateBeneficiary.and.returnValue( + of({ statusCode: 200, data: { response: 'Updated' } }) + ); + component.updateBeneficiaryDetails(); + expect( + registrar.updateBeneficiary.calls.mostRecent().args[0].passToNurse + ).toBeFalse(); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + }); + + it('updateBeneficiaryDetails failure alerts error', () => { + registrar.updateBeneficiary.and.returnValue( + of({ statusCode: 5000, errorMessage: 'nope' }) + ); + component.updateBeneficiaryDetails(); + expect(confirmation.alert).toHaveBeenCalledWith('nope', 'error'); + }); + + it('updateBeneficiaryDetails skips service when invalid', () => { + spyOn(component, 'checkValids').and.returnValue(false); + component.updateBeneficiaryDetails(); + expect(registrar.updateBeneficiary).not.toHaveBeenCalled(); + }); + }); + + describe('canDeactivate', () => { + it('returns of(true) when pristine', done => { + component.canDeactivate().subscribe(v => { + expect(v).toBeTrue(); + expect(confirmation.confirm).not.toHaveBeenCalled(); + done(); + }); + }); + it('asks for confirmation when dirty', done => { + component.beneficiaryRegistrationForm.markAsDirty(); + confirmation.confirm.and.returnValue(of(false)); + component.canDeactivate().subscribe(v => { + expect(v).toBeFalse(); + expect(confirmation.confirm).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('submit button calls postButtonCall', () => { + spyOn(component, 'postButtonCall'); + ( + fixture.nativeElement.querySelector('#submitButton') as HTMLElement + ).click(); + expect(component.postButtonCall).toHaveBeenCalled(); + }); + + it('redirectToSearch alerts asynchronously and navigates', fakeAsync(() => { + component.redirectToSearch(); + expect(router.navigate).toHaveBeenCalledWith(['/registrar/search/']); + flush(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issueInFetchDetails, + 'info' + ); + })); }); }); diff --git a/src/app/app-modules/registrar/search-dialog/search-dialog.component.spec.ts b/src/app/app-modules/registrar/search-dialog/search-dialog.component.spec.ts index 24b4d1b2..14a776bb 100644 --- a/src/app/app-modules/registrar/search-dialog/search-dialog.component.spec.ts +++ b/src/app/app-modules/registrar/search-dialog/search-dialog.component.spec.ts @@ -20,27 +20,242 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, + autoSpy, + commonTestProviders, +} from 'src/testing/test-utils'; +import { ConfirmationService } from '../../core/services/confirmation.service'; +import { CommonService } from '../../core/services/common-services.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { RegistrarService } from '../shared/services/registrar.service'; import { SearchDialogComponent } from './search-dialog.component'; +const LOCATION = { + stateMaster: [{ stateID: 1, stateName: 'S1' }], + otherLoc: { stateID: 1, districtList: [{ districtID: 4 }] }, +}; + +function master() { + return { + govIdEntityMaster: [{ govtIdentityTypeID: 1 }], + otherGovIdEntityMaster: [{ govtIdentityTypeID: 7 }], + genderMaster: [{ genderID: 1, genderName: 'Male' }], + }; +} + describe('SearchDialogComponent', () => { let component: SearchDialogComponent; let fixture: ComponentFixture; + let registrar: any; + let common: any; + let confirmation: any; + let dialogRef: any; + let session: any; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + registrar = autoSpy(RegistrarService, { + registrationMasterDetails$: new BehaviorSubject(null), + }); + common = autoSpy(CommonService); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [SearchDialogComponent], - }).compileComponents(); - })); - - beforeEach(() => { + providers: [ + ...commonTestProviders({ + session: { location: JSON.stringify(LOCATION) }, + }), + { provide: RegistrarService, useValue: registrar }, + { provide: CommonService, useValue: common }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(SearchDialogComponent, '') + .compileComponents(); fixture = TestBed.createComponent(SearchDialogComponent); component = fixture.componentInstance; + confirmation = TestBed.inject(ConfirmationService) as any; + dialogRef = TestBed.inject(MatDialogRef) as any; + session = TestBed.inject(SessionStorageService) as any; fixture.detectChanges(); }); - it('should be created', () => { - expect(component).toBeTruthy(); + afterEach(() => fixture.destroy()); + + it('initialises language, form, master request and states', () => { + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); + expect(Object.keys(component.newSearchForm.controls)).toEqual([ + 'firstName', + 'lastName', + 'fatherName', + 'dob', + 'gender', + 'stateID', + 'districtID', + ]); + expect(component.newSearchForm.valid).toBeFalse(); + expect(registrar.getRegistrationMaster).toHaveBeenCalledWith(1); + expect(component.states).toEqual(LOCATION.stateMaster); + expect(component.locations).toEqual(LOCATION); + expect(component.today).toEqual(jasmine.any(Date)); + expect(component.masterData).toBeUndefined(); + }); + + it('form becomes valid with required fields', () => { + component.newSearchForm.patchValue({ + firstName: 'A', + gender: 1, + stateID: 1, + districtID: 2, + }); + expect(component.newSearchForm.valid).toBeTrue(); + }); + + it('merges gov and other gov IDs when master data arrives', () => { + registrar.registrationMasterDetails$.next(master()); + expect(component.masterData).toBeTruthy(); + expect(component.govtIDs).toEqual([ + { govtIdentityTypeID: 1 }, + { govtIdentityTypeID: 7 }, + ]); + }); + + it('getStatesData leaves states untouched when no location is stored', () => { + session.store.delete('location'); + component.states = 'prev'; + component.getStatesData(); + expect(component.locations).toBeNull(); + expect(component.states).toBe('prev'); + }); + + it('resetBeneficiaryForm clears form and reloads states', () => { + component.newSearchForm.patchValue({ firstName: 'x' }); + component.states = null; + component.resetBeneficiaryForm(); + expect(component.newSearchForm.value.firstName).toBeNull(); + expect(component.states).toEqual(LOCATION.stateMaster); + }); + + describe('onStateChange', () => { + it('does nothing without a state', () => { + component.onStateChange(); + expect(registrar.getDistrictList).not.toHaveBeenCalled(); + }); + + it('loads districts', () => { + component.newSearchForm.patchValue({ stateID: 1 }); + registrar.getDistrictList.and.returnValue( + of({ statusCode: 200, data: [{ districtID: 4 }] }) + ); + component.onStateChange(); + expect(registrar.getDistrictList).toHaveBeenCalledWith(1); + expect(component.districts).toEqual([{ districtID: 4 }]); + }); + + it('alerts and closes dialog on failure', () => { + component.newSearchForm.patchValue({ stateID: 1 }); + registrar.getDistrictList.and.returnValue(of({ statusCode: 500 })); + component.onStateChange(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issueFetching, + 'error' + ); + expect(dialogRef.close).toHaveBeenCalledWith(false); + }); + }); + + it('getDistricts uses the common service', () => { + common.getDistricts.and.returnValue(of([{ districtID: 9 }])); + component.getDistricts(3); + expect(common.getDistricts).toHaveBeenCalledWith(3); + expect(component.districts).toEqual([{ districtID: 9 }]); + }); + + it('selectGender iterates master without matching (compares to control object)', () => { + component.masterData = master(); + component.newSearchForm.patchValue({ gender: 1 }); + component.selectGender(); + expect(component.newSearchForm.contains('genderName')).toBeFalse(); + }); + + it('selectGender assigns genderName only when genderID is the control itself', () => { + const genderCtrl = component.newSearchForm.controls['gender']; + component.masterData = { + genderMaster: [ + { genderID: 2, genderName: 'Female' }, + { genderID: genderCtrl, genderName: 'Male' }, + ], + }; + component.selectGender(); + expect((component.newSearchForm.controls as any)['genderName']).toBe( + 'Male' + ); + }); + + it('getStatesData otherLoc branch only runs for a non-primitive stored value (reads otherLoc off the raw string)', () => { + // getStatesData checks `location.otherLoc` on the raw (unparsed) value, + // so a plain JSON string never enters the branch. Use a String object + // that carries otherLoc to exercise it. + const raw: any = new String(JSON.stringify(LOCATION)); + raw.otherLoc = LOCATION.otherLoc; + session.getItem.and.returnValue(raw); + spyOn(component, 'onStateChange'); + component.getStatesData(); + expect(component.states).toEqual(LOCATION.stateMaster); + // controls are overwritten with raw values (current behaviour) + expect((component.newSearchForm.controls as any)['stateID']).toBe(1); + expect((component.newSearchForm.controls as any)['districtID']).toBe(4); + expect(component.onStateChange).toHaveBeenCalled(); + }); + + it('getStatesData with plain JSON string skips the otherLoc defaults', () => { + spyOn(component, 'onStateChange'); + component.getStatesData(); + expect(component.onStateChange).not.toHaveBeenCalled(); + expect(component.newSearchForm.controls['stateID'].value).toBeNull(); + }); + + it('onIDCardSelected is a no-op', () => { + expect(component.onIDCardSelected()).toBeUndefined(); + }); + + it('getSearchResult closes dialog with mapped search object', () => { + component.getSearchResult({ + firstName: 'A', + lastName: 'B', + fatherName: 'C', + dob: 'D', + gender: 1, + stateID: 2, + districtID: 3, + }); + const expected = { + firstName: 'A', + lastName: 'B', + fatherName: 'C', + dob: 'D', + genderID: 1, + i_bendemographics: { stateID: 2, districtID: 3 }, + }; + expect(component.dataObj).toEqual(expected); + expect(dialogRef.close).toHaveBeenCalledWith(expected); + }); + + it('AfterViewChecked runs change detection', () => { + const cdr = (component as any).changeDetectorRef; + spyOn(cdr, 'detectChanges'); + component.AfterViewChecked(); + expect(cdr.detectChanges).toHaveBeenCalled(); + }); + + it('ngDoCheck refreshes language', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toEqual(LANGUAGE_EN); }); }); diff --git a/src/app/app-modules/registrar/search/search.component.spec.ts b/src/app/app-modules/registrar/search/search.component.spec.ts index d0275517..6bc07791 100644 --- a/src/app/app-modules/registrar/search/search.component.spec.ts +++ b/src/app/app-modules/registrar/search/search.component.spec.ts @@ -21,267 +21,91 @@ */ import { - async, ComponentFixture, - tick, - inject, - fakeAsync, TestBed, + waitForAsync, + fakeAsync, + tick, } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA, ChangeDetectorRef } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Router } from '@angular/router'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { of, throwError } from 'rxjs'; + +import { SearchComponent } from './search.component'; import { ConfirmationService } from '../../core/services/confirmation.service'; -import { CameraService } from '../../core/services/camera.service'; import { RegistrarService } from '../shared/services/registrar.service'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/from'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/throw'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { SearchComponent } from './search.component'; -import { Router, ActivatedRoute } from '@angular/router'; -import { Md2Module } from 'md2'; -import { MaterialModule } from '../../core/material.module'; -import { BeneficiaryDetailsService } from 'app/app-modules/core/services/beneficiary-details.service'; - -class RouterStub { - navigateByUrl(url: string) { - return url; - } - - navigate(someRoute) {} -} -class MockActivatedRoute { - snapshot = { - params: [null], - }; -} - -class CameraStub { - viewImage(imagedata) {} -} - -class BeneficiaryServiceMock { - getBeneficiaryImage(id) {} -} - -class RegistrarServiceMock { - patientRevisit() {} - - quickSearch() {} - // submitBeneficiary = jasmine.createSpy('submitBeneficiary').andCallFake(function () { - // return Observable.of(true); - // }); - // updateBeneficiary = jasmine.createSpy('updateBeneficiary').andCallFake(function () { - // return Observable.of(true); - // }); - - registrationMaster = { - occupationMaster: [ - { occupationID: 2, occupationType: 'Agricultural labour' }, - { occupationID: 3, occupationType: 'Business' }, - { occupationID: 1, occupationType: 'Cultivation(Agriculture)' }, - { occupationID: 5, occupationType: 'Govt employee' }, - { occupationID: 9, occupationType: 'Housewife or Homemaker' }, - { occupationID: 8, occupationType: 'Not Applicable' }, - { occupationID: 7, occupationType: 'Other' }, - { occupationID: 6, occupationType: 'Private employee' }, - { occupationID: 4, occupationType: 'Student' }, - ], - incomeMaster: [ - { incomeStatusID: 1, incomeStatus: 'APL' }, - { incomeStatusID: 2, incomeStatus: 'BPL' }, - { incomeStatusID: 3, incomeStatus: "Don't Know" }, - ], - maritalStatusMaster: [ - { maritalStatusID: 3, status: 'Divorced' }, - { maritalStatusID: 2, status: 'Married' }, - { maritalStatusID: 7, status: 'Not Applicable' }, - { maritalStatusID: 4, status: 'Seperated' }, - { maritalStatusID: 1, status: 'Unmarried' }, - { maritalStatusID: 5, status: 'Widow' }, - { maritalStatusID: 6, status: 'Widower' }, - ], - religionMaster: [ - { religionID: 5, religionType: 'Buddhism' }, - { religionID: 1, religionType: 'Hinduism' }, - { religionID: 2, religionType: 'Islam' }, - { religionID: 6, religionType: 'Jainism' }, - { religionID: 3, religionType: 'Judaism' }, - { religionID: 7, religionType: 'Other' }, - { religionID: 4, religionType: 'Sikhism' }, - ], - communityMaster: [ - { communityID: 4, communityType: 'BC' }, - { communityID: 7, communityType: "Dont' Know" }, - { communityID: 5, communityType: 'OBC' }, - { communityID: 6, communityType: 'OC' }, - { communityID: 2, communityType: 'SC' }, - { communityID: 3, communityType: 'ST' }, - ], - qualificationMaster: [ - { educationID: 6, educationType: 'Diploma / Under Graduate' }, - { educationID: 7, educationType: 'Graduate' }, - { educationID: 8, educationType: 'Post Graduate & Above' }, - { educationID: 2, educationType: 'Primary (1st to 5th Std)' }, - { educationID: 4, educationType: 'Secondary (9th to 10th)' }, - { - educationID: 5, - educationType: 'Senior Secondary (11th to 12th/Intermediate)', - }, - { educationID: 3, educationType: 'Upper Primary (6th to 8th Std)' }, - ], - govIdEntityMaster: [ - { govtIdentityTypeID: 1, identityType: 'Aadhar' }, - { govtIdentityTypeID: 3, identityType: 'Driving License' }, - { govtIdentityTypeID: 4, identityType: 'PAN' }, - { govtIdentityTypeID: 5, identityType: 'Passport' }, - { govtIdentityTypeID: 6, identityType: 'Ration Card' }, - { govtIdentityTypeID: 2, identityType: 'Voter ID' }, - ], - genderMaster: [ - { genderID: 2, genderName: 'Female' }, - { genderID: 1, genderName: 'Male' }, - { genderID: 3, genderName: 'Transgender' }, - ], - otherGovIdEntityMaster: [ - { govtIdentityTypeID: 7, identityType: 'Assam Arogya Nidhi (AAN)' }, - { govtIdentityTypeID: 8, identityType: 'Atal Amrit Abhiyan' }, - { - govtIdentityTypeID: 9, - identityType: 'Janani Shishu Suraksha Karyakram (JSSK)', - }, - { govtIdentityTypeID: 10, identityType: 'Janani Suraksha Yojana (JSY)' }, - { govtIdentityTypeID: 11, identityType: 'KAYAKALP' }, - { govtIdentityTypeID: 12, identityType: 'RMNCH+A' }, - { - govtIdentityTypeID: 13, - identityType: 'Sanjeevani - Village Health Outreach Programme', - }, - { govtIdentityTypeID: 14, identityType: 'Sneha Sparsha' }, - ], - }; - registrationMasterDetails = new BehaviorSubject(null); - registrationMasterDetails$ = this.registrationMasterDetails.asObservable(); - - getRegistrationMaster(spID: any) { - this.registrationMasterDetails.next(this.registrationMaster); - } - - submitBeneficiary() { - return Observable.of(true); - } - updateBeneficiary() { - return Observable.of(false); - } -} - -describe('SearchComponent', () => { +import { CameraService } from '../../core/services/camera.service'; +import { BeneficiaryDetailsService } from '../../core/services/beneficiary-details.service'; +import { HttpServiceService } from '../../core/services/http-service.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +describe('SearchComponent (Registrar)', () => { let component: SearchComponent; let fixture: ComponentFixture; - let router: Router; - let route: ActivatedRoute; - let camera: CameraService; - let registrarService: RegistrarService; - let beneficiaryService: BeneficiaryDetailsService; - let confirmService: ConfirmationService; - let el: HTMLElement; - let spy: any; - let button: DebugElement; - let quickSearchButton: DebugElement; - const searchForOm = [ - { - beneficiaryRegID: 7432, - benName: 'SOMIYA RABHA ', - age: '8 months - 10 days', - }, - { - beneficiaryRegID: 7472, - benName: 'SOMEONE SOMESURNAME', - genderID: 2, - genderName: 'Female', - districtName: 'BARPETA', - villageName: '2 No.Chachara', - phoneNo: '5678987654', - age: '34 years - 2 months', - }, - { - beneficiaryRegID: 7505, - benName: 'OMI ', - genderID: 2, - genderName: 'Female', - fatherName: '', - districtName: 'BARPETA', - villageName: '2 No.Chachara', - phoneNo: '9987654396', - age: '23 years - 2 months', - }, - { - beneficiaryRegID: 7928, - benName: 'OM KUMARI', - genderID: 2, - genderName: 'Female', - fatherName: 'SKRAO', - districtName: 'CHITTOOR', - villageName: 'Ithepalle', - phoneNo: '9789678598', - age: '41 years - 1 months', - }, - { - beneficiaryRegID: 7930, - benName: 'OM KUMARI', - genderID: 2, - genderName: 'Female', - districtName: 'CHITTOOR', - villageName: 'Chandragiri', - phoneNo: '9789678564', - age: '41 years - 1 months', - }, - { - beneficiaryRegID: 8040, - benName: 'RANDOM ', - genderID: 2, - genderName: 'Female', - fatherName: '', - districtName: 'BALOD', - phoneNo: '3434343456', - age: '22 years - 0 months', - }, - { - beneficiaryRegID: 8040, - benName: 'RANDOM ', - genderID: 2, - genderName: 'Female', - fatherName: '', - districtName: 'BALOD', - age: '22 years - 0 months', - }, - ]; - - beforeEach(async(() => { + let mockConfirmationService: jasmine.SpyObj; + let mockRegistrarService: jasmine.SpyObj; + let mockCameraService: jasmine.SpyObj; + let mockBeneficiaryDetailsService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockDialog: jasmine.SpyObj; + let mockHttpService: jasmine.SpyObj; + let mockSessionStorage: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + 'confirm', + ]); + mockRegistrarService = jasmine.createSpyObj('RegistrarService', [ + 'identityQuickSearch', + 'advanceSearchIdentity', + 'identityPatientRevisit', + 'saveBeneficiaryEditDataASobservable', + ]); + mockCameraService = jasmine.createSpyObj('CameraService', ['viewImage']); + mockBeneficiaryDetailsService = jasmine.createSpyObj( + 'BeneficiaryDetailsService', + ['getBeneficiaryImage', 'reset'] + ); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + Object.defineProperty(mockRouter, 'routerState', { + get: () => ({ snapshot: { url: '/registrar/search' } }), + }); + mockDialog = jasmine.createSpyObj('MatDialog', ['open']); + mockHttpService = jasmine.createSpyObj('HttpServiceService', [ + 'setLanguage', + ]); + (mockHttpService as any).currentLangugae$ = of({}); + mockSessionStorage = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockSessionStorage.getItem.and.callFake((key: string) => { + if (key === 'serviceLineDetails') + return JSON.stringify({ vanID: 100, parkingPlaceID: 10 }); + if (key === 'providerServiceID') return '123'; + return null; + }); + TestBed.configureTestingModule({ - schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], - imports: [ - ReactiveFormsModule, - FormsModule, - Md2Module, - MaterialModule, - NoopAnimationsModule, - ], + imports: [FormsModule, ReactiveFormsModule], declarations: [SearchComponent], + schemas: [NO_ERRORS_SCHEMA], providers: [ - { provide: RegistrarService, useClass: RegistrarServiceMock }, - ConfirmationService, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: RegistrarService, useValue: mockRegistrarService }, + { provide: CameraService, useValue: mockCameraService }, { provide: BeneficiaryDetailsService, - useClass: BeneficiaryServiceMock, + useValue: mockBeneficiaryDetailsService, }, - { provide: CameraService, useClass: CameraStub }, - { provide: Router, useClass: RouterStub }, - { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: Router, useValue: mockRouter }, + { provide: MatDialog, useValue: mockDialog }, + { provide: HttpServiceService, useValue: mockHttpService }, + { provide: SessionStorageService, useValue: mockSessionStorage }, + { provide: ChangeDetectorRef, useValue: { detectChanges: () => {} } }, ], }).compileComponents(); })); @@ -289,242 +113,604 @@ describe('SearchComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SearchComponent); component = fixture.componentInstance; - registrarService = TestBed.get(RegistrarService); - confirmService = TestBed.get(ConfirmationService); - camera = TestBed.get(CameraService); - router = TestBed.get(Router); - route = TestBed.get(ActivatedRoute); - beneficiaryService = TestBed.get(BeneficiaryDetailsService); - //button = fixture.debugElement.queryAll(By.css('button')); - //quickSearchButton = button[0]; fixture.detectChanges(); }); - it('search component should be created', () => { + it('should create the component', () => { expect(component).toBeTruthy(); }); - it('should call Quick Search without any search value and give alert to user', () => { - const spier = spyOn(confirmService, 'alert').and.returnValue(1); - component.quickSearch(); - expect(spier).toHaveBeenCalledWith( - 'Please enter Beneficiary ID, Name or Phone No first...' - ); - }); - - it('should call Quick Search when search button is clicked', fakeAsync(() => { - const spier = spyOn(component, 'quickSearch').and.callThrough(); - const compiled = fixture.debugElement.nativeElement; - compiled.querySelector('button').click(); - tick(); - fixture.detectChanges(); - expect(spier).toHaveBeenCalled(); - })); - - it('should call Quick Search with 1 char in search value and give alert to user', () => { - const spier = spyOn(confirmService, 'alert').and.returnValue(1); - component.quickSearch('b'); - expect(spier).toHaveBeenCalledWith( - 'Minimum 2 Character is required to search Beneficiary !!!' - ); - }); - - it('should call registrar service when 2 chars are given for searching', () => { - const spier = spyOn(registrarService, 'quickSearch').and.returnValue( - Observable.of(1) - ); - component.quickSearch('ab'); - expect(spier).toHaveBeenCalledWith({ benID: 'ab' }); + it('should set searchPattern on init', () => { + expect(component.searchPattern).toBeDefined(); + expect(component.searchPattern).toContain('a-zA-Z0-9'); }); - it('should call alert when nothing is returned by search service', () => { - const spier = spyOn(registrarService, 'quickSearch').and.returnValue( - Observable.of(null) - ); - const spiAlert = spyOn(confirmService, 'alert').and.returnValue(1); - component.quickSearch('ab'); - expect(spiAlert).toHaveBeenCalledWith('Beneficiary Not found...'); - }); - - it('should load data returned by search service into beneficiarylist', () => { - const spier = spyOn(registrarService, 'quickSearch').and.returnValue( - Observable.of(searchForOm) - ); - component.quickSearch('OM'); - fixture.autoDetectChanges(); - expect(component.beneficiaryList).toBe(searchForOm); - }); - - it('should set default filtered list to be equal to beneficiarylist', () => { - const spier = spyOn(registrarService, 'quickSearch').and.returnValue( - Observable.of(searchForOm) - ); - component.quickSearch('OM'); - fixture.autoDetectChanges(); - expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); - }); - - it('should set filtered list to be equal to beneficiarylist when filter is called without any parameter', () => { - component.filteredBeneficiaryList = []; - component.beneficiaryList = searchForOm; - component.filterBeneficiaryList(); - fixture.autoDetectChanges(); - expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); - }); - - it('should set filtered list empty as per the searched query as no matching pattern is found', () => { - component.filteredBeneficiaryList = []; - component.beneficiaryList = searchForOm; - component.filterBeneficiaryList('qwertyuiop'); - fixture.autoDetectChanges(); - expect(component.filteredBeneficiaryList).toEqual([]); - }); - - it('should confirm when user tries to submit patient to nurse', () => { - const spier = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(false) - ); - component.patientRevisited(123); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith( - `Confirm Patient Revisit`, - `Please Confirm to Submit Beneficiary to Nurse Work-List ?` - ); + describe('identityQuickSearch', () => { + beforeEach(() => { + component.currentLanguageSet = { + alerts: { + info: { + pleaseenterBeneficiaryID: 'Please enter ID', + beneficiarynotfound: 'Not found', + phoneDetails: 'Enter 10 or 12 digit number', + }, + }, + }; + }); + + it('should alert when searchTerm is undefined', () => { + component.identityQuickSearch(undefined); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Please enter ID', + 'info' + ); + }); + + it('should alert when searchTerm is empty string', () => { + component.identityQuickSearch(''); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Please enter ID', + 'info' + ); + }); + + it('should alert when searchTerm is whitespace', () => { + component.identityQuickSearch(' '); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Please enter ID', + 'info' + ); + }); + + it('should alert phoneDetails when searchTerm length is not 10 or 12', () => { + component.identityQuickSearch('12345'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Enter 10 or 12 digit number', + 'info' + ); + }); + + it('should search by phoneNo when searchTerm length is 10', () => { + mockRegistrarService.identityQuickSearch.and.returnValue(of([])); + component.identityQuickSearch('1234567890'); + expect(mockRegistrarService.identityQuickSearch).toHaveBeenCalledWith( + jasmine.objectContaining({ phoneNo: '1234567890' }) + ); + }); + + it('should search by beneficiaryID when searchTerm length is 12', () => { + mockRegistrarService.identityQuickSearch.and.returnValue(of([])); + component.identityQuickSearch('123456789012'); + expect(mockRegistrarService.identityQuickSearch).toHaveBeenCalledWith( + jasmine.objectContaining({ beneficiaryID: '123456789012' }) + ); + }); + + it('should alert when no beneficiaries are found', () => { + mockRegistrarService.identityQuickSearch.and.returnValue(of([])); + component.identityQuickSearch('1234567890'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Not found', + 'info' + ); + }); + + it('should alert when null is returned', () => { + mockRegistrarService.identityQuickSearch.and.returnValue(of(null as any)); + component.identityQuickSearch('1234567890'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Not found', + 'info' + ); + }); + + it('should populate beneficiaryList on successful search', () => { + const mockData = { + data: [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 100, + firstName: 'John', + lastName: 'Doe', + m_gender: { genderName: 'Male' }, + fatherName: 'Father', + i_bendemographics: { + districtName: 'District1', + districtBranchName: 'Village1', + }, + benPhoneMaps: [{ phoneNo: '1234567890' }], + dOB: '1990-01-01', + createdDate: '2023-01-01', + }, + ], + }; + mockRegistrarService.identityQuickSearch.and.returnValue(of(mockData)); + component.identityQuickSearch('1234567890'); + expect(component.beneficiaryList).toBeDefined(); + expect(component.beneficiaryList.length).toBe(1); + expect(component.beneficiaryList[0].beneficiaryID).toBe('BEN001'); + }); + + it('should alert on service error', () => { + mockRegistrarService.identityQuickSearch.and.returnValue( + throwError('Network error') + ); + component.identityQuickSearch('1234567890'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); }); - it('should call sendToNurseWindow function if user confirms for revisit', () => { - const spi = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(true) - ); - const spier = spyOn(component, 'sendToNurseWindow').and.returnValue(1); - component.patientRevisited(123); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalled(); + describe('searchRestruct', () => { + it('should restructure beneficiary data for table display', () => { + const benList = { + data: [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 100, + firstName: 'John', + lastName: 'Doe', + m_gender: { genderName: 'Male' }, + fatherName: 'Father', + i_bendemographics: { + districtName: 'District1', + districtBranchName: 'Village1', + }, + benPhoneMaps: [{ phoneNo: '9876543210' }], + dOB: '1990-01-01', + createdDate: '2023-06-15', + }, + ], + }; + const result = component.searchRestruct(benList, {}); + expect(result.length).toBe(1); + expect(result[0].benName).toBe('John Doe'); + expect(result[0].genderName).toBe('Male'); + expect(result[0].fatherName).toBe('Father'); + expect(result[0].districtName).toBe('District1'); + expect(result[0].villageName).toBe('Village1'); + expect(result[0].phoneNo).toBe('9876543210'); + }); + + it('should show Not Available for missing gender', () => { + const benList = { + data: [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 100, + firstName: 'John', + lastName: '', + m_gender: { genderName: '' }, + fatherName: '', + i_bendemographics: { + districtName: '', + districtBranchName: '', + }, + benPhoneMaps: [], + dOB: '1990-01-01', + createdDate: '2023-01-01', + }, + ], + }; + const result = component.searchRestruct(benList, {}); + expect(result[0].phoneNo).toBe('Not Available'); + }); }); - it('should not call sendToNurseWindow function if user doesnt confirm for revisit', () => { - const spi = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(false) - ); - const spier = spyOn(component, 'sendToNurseWindow').and.returnValue(1); - component.patientRevisited(123); - fixture.autoDetectChanges(); - expect(spier).not.toHaveBeenCalled(); + describe('getCorrectPhoneNo', () => { + it('should return Not Available when phoneMaps is empty', () => { + expect(component.getCorrectPhoneNo([], {})).toBe('Not Available'); + }); + + it('should return first phoneNo when benObject has no phoneNo', () => { + const phoneMaps = [{ phoneNo: '111' }, { phoneNo: '222' }]; + expect(component.getCorrectPhoneNo(phoneMaps, {})).toBe('111'); + }); + + it('should return matching phoneNo when benObject has phoneNo', () => { + const phoneMaps = [{ phoneNo: '111' }, { phoneNo: '222' }]; + expect(component.getCorrectPhoneNo(phoneMaps, { phoneNo: '222' })).toBe( + '222' + ); + }); + + it('should return first phoneNo if no match found', () => { + const phoneMaps = [{ phoneNo: '111' }, { phoneNo: '222' }]; + expect(component.getCorrectPhoneNo(phoneMaps, { phoneNo: '333' })).toBe( + '111' + ); + }); }); - it('should ask for confirmation if used wants to edit the registration', () => { - const spier = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(false) - ); - component.editPatientInfo(123); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith( - `Confirm Beneficiary Editing`, - `Please Confirm do you really want to edit details of this Beneficiary?` - ); - }); - - it('should navigate to editing page if user confirms editing registration', () => { - const spi = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(true) - ); - const spier = spyOn(router, 'navigate').and.returnValue(1); - const benRegID = 123; - component.editPatientInfo(benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith(['/registrar/search/' + benRegID]); + describe('filterBeneficiaryList', () => { + beforeEach(() => { + component.beneficiaryList = [ + { + beneficiaryID: 'BEN001', + benName: 'John Doe', + genderName: 'Male', + benObject: {}, + }, + { + beneficiaryID: 'BEN002', + benName: 'Jane Smith', + genderName: 'Female', + benObject: {}, + }, + ]; + }); + + it('should reset to full list when searchTerm is empty', () => { + component.filteredBeneficiaryList = []; + component.filterBeneficiaryList(''); + expect(component.filteredBeneficiaryList).toBe(component.beneficiaryList); + }); + + it('should filter by matching name', () => { + component.filterBeneficiaryList('John'); + expect(component.filteredBeneficiaryList.length).toBe(1); + expect(component.filteredBeneficiaryList[0].benName).toBe('John Doe'); + }); + + it('should return empty list when no match found', () => { + component.filterBeneficiaryList('ZZZZZ'); + expect(component.filteredBeneficiaryList.length).toBe(0); + }); + + it('should not filter by benObject key', () => { + component.filterBeneficiaryList('object'); + expect(component.filteredBeneficiaryList.length).toBe(0); + }); + + it('should filter case-insensitively', () => { + component.filterBeneficiaryList('john'); + expect(component.filteredBeneficiaryList.length).toBe(1); + }); }); - it('should not navigate to editing page if user doesnt confirm editing registration', () => { - const spi = spyOn(confirmService, 'confirm').and.returnValue( - Observable.of(false) - ); - const spier = spyOn(router, 'navigate').and.returnValue(1); - const benRegID = 123; - component.editPatientInfo(benRegID); - fixture.autoDetectChanges(); - expect(spier).not.toHaveBeenCalled(); + describe('patientRevisited', () => { + beforeEach(() => { + component.currentLanguageSet = { + alerts: { + info: { + confirmSubmitBeneficiary: 'Confirm submit', + genderAndAgeDetails: 'No gender and age', + noGenderDetails: 'No gender', + noAgeDetailsAvail: 'No age', + }, + }, + }; + }); + + it('should confirm and call sendToNurseWindow when benObject is valid', () => { + const benObject = { + m_gender: { genderName: 'Male' }, + dOB: '1990-01-01', + }; + mockConfirmationService.confirm.and.returnValue(of(true)); + spyOn(component, 'sendToNurseWindow'); + component.patientRevisited(benObject); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + expect(component.sendToNurseWindow).toHaveBeenCalledWith(true, benObject); + }); + + it('should not call sendToNurseWindow when user declines', () => { + const benObject = { + m_gender: { genderName: 'Male' }, + dOB: '1990-01-01', + }; + mockConfirmationService.confirm.and.returnValue(of(false)); + spyOn(component, 'sendToNurseWindow'); + component.patientRevisited(benObject); + expect(component.sendToNurseWindow).not.toHaveBeenCalled(); + }); + + it('should alert when both gender and dOB are missing', () => { + const benObject = { m_gender: { genderName: '' }, dOB: '' }; + component.patientRevisited(benObject); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No gender and age', + 'info' + ); + }); + + it('should alert when only gender is missing', () => { + const benObject = { + m_gender: { genderName: '' }, + dOB: '1990-01-01', + }; + component.patientRevisited(benObject); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No gender', + 'info' + ); + }); + + it('should alert when only dOB is missing', () => { + const benObject = { m_gender: { genderName: 'Male' }, dOB: '' }; + component.patientRevisited(benObject); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'No age', + 'info' + ); + }); }); - it('should call registrarService.patientRevisit if sendToNurseWindows gets user permission', () => { - const res = 'sent to nurse window'; - const spi = spyOn(confirmService, 'alert').and.returnValue(1); - const spier = spyOn(registrarService, 'patientRevisit').and.returnValue( - Observable.of({ response: res }) - ); - const benRegID = 123; - component.sendToNurseWindow(true, benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith({ beneficiaryRegID: benRegID }); + describe('editPatientInfo', () => { + beforeEach(() => { + component.currentLanguageSet = { + alerts: { info: { editDetails: 'Edit details?' } }, + }; + }); + + it('should navigate on confirmation', () => { + mockConfirmationService.confirm.and.returnValue(of(true)); + const beneficiary = { + beneficiaryID: 'BEN001', + benObject: { name: 'John' }, + }; + component.editPatientInfo(beneficiary); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/registrar/search/BEN001', + ]); + expect( + mockRegistrarService.saveBeneficiaryEditDataASobservable + ).toHaveBeenCalledWith(beneficiary.benObject); + }); + + it('should not navigate when user declines', () => { + mockConfirmationService.confirm.and.returnValue(of(false)); + const beneficiary = { + beneficiaryID: 'BEN001', + benObject: { name: 'John' }, + }; + component.editPatientInfo(beneficiary); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); }); - it('should alert after calling registrarService.patientRevisit if sendToNurseWindows gets user permission', () => { - const res = 'sent to nurse window'; - const spi = spyOn(registrarService, 'patientRevisit').and.returnValue( - Observable.of({ response: res }) - ); - const spier = spyOn(confirmService, 'alert').and.returnValue(1); - const benRegID = 123; - component.sendToNurseWindow(true, benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith(res); + describe('sendToNurseWindow', () => { + it('should call identityPatientRevisit when userResponse is true', () => { + const benObject = { beneficiaryRegID: 100 }; + mockRegistrarService.identityPatientRevisit.and.returnValue( + of({ data: { response: 'Success' } }) + ); + component.sendToNurseWindow(true, benObject); + expect(mockRegistrarService.identityPatientRevisit).toHaveBeenCalledWith( + benObject + ); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Success', + 'success' + ); + }); + + it('should alert with status when data is missing', () => { + const benObject = { beneficiaryRegID: 100 }; + mockRegistrarService.identityPatientRevisit.and.returnValue( + of({ status: 'Warning' }) + ); + component.sendToNurseWindow(true, benObject); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Warning', + 'warn' + ); + }); + + it('should not call identityPatientRevisit when userResponse is false', () => { + component.sendToNurseWindow(false, {}); + expect( + mockRegistrarService.identityPatientRevisit + ).not.toHaveBeenCalled(); + }); + + it('should alert on error', () => { + const benObject = { beneficiaryRegID: 100 }; + mockRegistrarService.identityPatientRevisit.and.returnValue( + throwError('Network error') + ); + component.sendToNurseWindow(true, benObject); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Network error', + 'error' + ); + }); }); - it('should not call registrarService.patientRevisit if sendToNurseWindows doenst get user permission', () => { - const res = 'sent to nurse window'; - const spier = spyOn(registrarService, 'patientRevisit').and.returnValue( - Observable.of({ response: res }) - ); - const benRegID = 123; - component.sendToNurseWindow(false, benRegID); - fixture.autoDetectChanges(); - expect(spier).not.toHaveBeenCalled(); + describe('patientImageView', () => { + beforeEach(() => { + component.currentLanguageSet = { + alerts: { info: { imageNotFound: 'No image' } }, + }; + }); + + it('should call getBeneficiaryImage and view image when found', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue( + of({ benImage: 'imageData' }) + ); + component.patientImageView(123); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).toHaveBeenCalledWith(123 as any); + expect(mockCameraService.viewImage).toHaveBeenCalledWith('imageData'); + }); + + it('should alert when image is not found', () => { + mockBeneficiaryDetailsService.getBeneficiaryImage.and.returnValue(of({})); + component.patientImageView(123); + expect(mockConfirmationService.alert).toHaveBeenCalledWith('No image'); + }); + + it('should not call service when benregID is empty', () => { + component.patientImageView(''); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + + it('should not call service when benregID is null', () => { + component.patientImageView(null); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); + + it('should not call service when benregID is undefined', () => { + component.patientImageView(undefined); + expect( + mockBeneficiaryDetailsService.getBeneficiaryImage + ).not.toHaveBeenCalled(); + }); }); - it('should call beneficiaryDetailsService.getBeneficiaryImage if patientImageView is called', () => { - const spier = spyOn( - beneficiaryService, - 'getBeneficiaryImage' - ).and.returnValue(Observable.of(1)); - const benRegID = 123; - component.patientImageView(benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith(benRegID); + describe('openSearchDialog', () => { + it('should open SearchDialogComponent dialog', () => { + const mockDialogRef = { + afterClosed: () => of(null), + } as any; + mockDialog.open.and.returnValue(mockDialogRef); + component.openSearchDialog(); + expect(mockDialog.open).toHaveBeenCalled(); + }); + + it('should call advanceSearchIdentity when dialog returns data', () => { + const dialogResult = { + firstName: 'John', + lastName: 'Doe', + gender: 1, + stateID: 10, + districtID: 20, + }; + const mockDialogRef = { + afterClosed: () => of(dialogResult), + } as any; + mockDialog.open.and.returnValue(mockDialogRef); + + const searchResult = { + data: [ + { + beneficiaryID: 'BEN001', + beneficiaryRegID: 100, + firstName: 'John', + lastName: 'Doe', + m_gender: { genderName: 'Male' }, + fatherName: 'Father', + i_bendemographics: { + districtName: 'District1', + districtBranchName: 'Village1', + }, + benPhoneMaps: [{ phoneNo: '1234567890' }], + dOB: '1990-01-01', + createdDate: '2023-01-01', + }, + ], + }; + mockRegistrarService.advanceSearchIdentity.and.returnValue( + of(searchResult) + ); + component.openSearchDialog(); + expect(mockRegistrarService.advanceSearchIdentity).toHaveBeenCalledWith( + dialogResult + ); + expect(component.beneficiaryList.length).toBe(1); + }); + + it('should alert when advance search returns no results', () => { + component.currentLanguageSet = { + alerts: { info: { beneficiarynotfound: 'Not found' } }, + }; + const mockDialogRef = { + afterClosed: () => of({ firstName: 'ZZZ' }), + } as any; + mockDialog.open.and.returnValue(mockDialogRef); + mockRegistrarService.advanceSearchIdentity.and.returnValue( + of({ data: [] }) + ); + component.openSearchDialog(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Not found', + 'info' + ); + }); + + it('should not call service when dialog is dismissed', () => { + const mockDialogRef = { + afterClosed: () => of(null), + } as any; + mockDialog.open.and.returnValue(mockDialogRef); + component.openSearchDialog(); + expect(mockRegistrarService.advanceSearchIdentity).not.toHaveBeenCalled(); + }); }); - it('should not call beneficiaryDetailsService.getBeneficiaryImage if patientImageView is called with benregID', () => { - const spier = spyOn( - beneficiaryService, - 'getBeneficiaryImage' - ).and.returnValue(Observable.of(1)); - component.patientImageView(''); - fixture.autoDetectChanges(); - expect(spier).not.toHaveBeenCalled(); + describe('navigateTORegistrar', () => { + it('should navigate directly when beneficiaryList is undefined', () => { + component.beneficiaryList = undefined; + component.navigateTORegistrar(); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/registrar/registration', + ]); + }); + + it('should navigate directly when beneficiaryList is empty', () => { + component.beneficiaryList = []; + component.navigateTORegistrar(); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/registrar/registration', + ]); + }); + + it('should confirm before navigating when beneficiaryList has data', () => { + component.beneficiaryList = [{ benName: 'John' }]; + mockConfirmationService.confirm.and.returnValue(of(true)); + component.navigateTORegistrar(); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalledWith([ + '/registrar/registration', + ]); + }); + + it('should not navigate when user declines confirmation', () => { + component.beneficiaryList = [{ benName: 'John' }]; + mockConfirmationService.confirm.and.returnValue(of(false)); + component.navigateTORegistrar(); + expect(mockConfirmationService.confirm).toHaveBeenCalled(); + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); }); - it('should call viewImage of Camera when getBeneficiaryImage is called on patientImageView ', () => { - const imageData = 'wdvsghbfhf3t463yhb'; - const spi = spyOn( - beneficiaryService, - 'getBeneficiaryImage' - ).and.returnValue(Observable.of(imageData)); - const spier = spyOn(camera, 'viewImage').and.callFake(() => {}); - const benRegID = 123; - component.patientImageView(benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalled(); + describe('pageChanged', () => { + it('should set pagedList from filteredBeneficiaryList', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + component.pageChanged({ page: 1, itemsPerPage: 2 }); + expect(component.pagedList.length).toBe(2); + }); + + it('should handle second page correctly', () => { + component.filteredBeneficiaryList = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + { id: 4 }, + { id: 5 }, + ]; + component.pageChanged({ page: 2, itemsPerPage: 2 }); + expect(component.pagedList.length).toBe(2); + }); }); - it('should call viewImage of Camera with data which getBeneficiaryImage returned on patientImageView call', () => { - const imageData = 'wdvsghbfhf3t463yhb'; - const spi = spyOn( - beneficiaryService, - 'getBeneficiaryImage' - ).and.returnValue(Observable.of({ benImage: imageData })); - const spier = spyOn(camera, 'viewImage').and.callFake(() => {}); - const benRegID = 123; - component.patientImageView(benRegID); - fixture.autoDetectChanges(); - expect(spier).toHaveBeenCalledWith(imageData); + describe('fetchLanguageResponse', () => { + it('should set currentLanguageSet', () => { + component.fetchLanguageResponse(); + expect(component.languageComponent).toBeDefined(); + }); }); }); diff --git a/src/app/app-modules/registrar/shared/services/registrar.service.spec.ts b/src/app/app-modules/registrar/shared/services/registrar.service.spec.ts new file mode 100644 index 00000000..eccc01ce --- /dev/null +++ b/src/app/app-modules/registrar/shared/services/registrar.service.spec.ts @@ -0,0 +1,366 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { RegistrarService } from './registrar.service'; +import { environment } from 'src/environments/environment'; + +describe('RegistrarService', () => { + let service: RegistrarService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [RegistrarService], + }); + service = TestBed.inject(RegistrarService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('BehaviorSubjects initial values', () => { + it('should initialise registrationMasterDetails with null', done => { + service.registrationMasterDetails$.subscribe(val => { + expect(val).toBeNull(); + done(); + }); + }); + + it('should initialise beneficiaryDetails with null', done => { + service.beneficiaryDetails$.subscribe(val => { + expect(val).toBeNull(); + done(); + }); + }); + + it('should initialise beneficiaryEditDetails with null', done => { + service.beneficiaryEditDetails$.subscribe(val => { + expect(val).toBeNull(); + done(); + }); + }); + }); + + describe('getRegistrationMaster', () => { + it('should POST to registrarMasterDataUrl and update registrationMasterDetails subject', () => { + const mockData = { data: { occupations: [], genders: [] } }; + let emitted: any = null; + service.registrationMasterDetails$.subscribe(val => (emitted = val)); + + service.getRegistrationMaster(101); + + const req = httpMock.expectOne(environment.registrarMasterDataUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ spID: 101 }); + req.flush(mockData); + + expect(emitted).toEqual(mockData.data); + }); + + it('should not update subject when response data is falsy', () => { + let emitted: any = null; + service.registrationMasterDetails$.subscribe(val => (emitted = val)); + + service.getRegistrationMaster(101); + + const req = httpMock.expectOne(environment.registrarMasterDataUrl); + req.flush({ data: null }); + + expect(emitted).toBeNull(); + }); + }); + + describe('getPatientDataAsObservable', () => { + it('should POST to getCompleteBeneficiaryDetail and update beneficiaryDetails subject', () => { + const mockData = { data: { name: 'John' } }; + let emitted: any = null; + service.beneficiaryDetails$.subscribe(val => (emitted = val)); + + service.getPatientDataAsObservable(123); + + const req = httpMock.expectOne(environment.getCompleteBeneficiaryDetail); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ beneficiaryRegID: 123 }); + req.flush(mockData); + + expect(emitted).toEqual(mockData.data); + }); + + it('should not update subject when response data is falsy', () => { + let emitted: any = null; + service.beneficiaryDetails$.subscribe(val => (emitted = val)); + + service.getPatientDataAsObservable(123); + + const req = httpMock.expectOne(environment.getCompleteBeneficiaryDetail); + req.flush({ data: null }); + + expect(emitted).toBeNull(); + }); + }); + + describe('getPatientData', () => { + it('should POST to getCompleteBeneficiaryDetail and return an observable', () => { + const mockResp = { data: { name: 'Jane' } }; + service.getPatientData(456).subscribe(res => { + expect(res).toEqual(mockResp); + }); + + const req = httpMock.expectOne(environment.getCompleteBeneficiaryDetail); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ beneficiaryRegID: 456 }); + req.flush(mockResp); + }); + }); + + describe('registerBeneficiary', () => { + it('should POST beneficiary data wrapped in benD key', () => { + const beneficiary = { firstName: 'A' }; + service.registerBeneficiary(beneficiary).subscribe(); + + const req = httpMock.expectOne(environment.registerBeneficiaryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ benD: beneficiary }); + req.flush({}); + }); + }); + + describe('quickSearch', () => { + it('should POST search term to quickSearchUrl', () => { + const searchTerm = { searchKey: 'John' }; + service.quickSearch(searchTerm).subscribe(); + + const req = httpMock.expectOne(environment.quickSearchUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(searchTerm); + req.flush({}); + }); + }); + + describe('identityQuickSearch', () => { + it('should POST search term to identityQuickSearchUrl', () => { + const searchTerm = { searchKey: 'Doe' }; + service.identityQuickSearch(searchTerm).subscribe(); + + const req = httpMock.expectOne(environment.identityQuickSearchUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(searchTerm); + req.flush({}); + }); + }); + + describe('advanceSearch', () => { + it('should POST search terms to advanceSearchUrl', () => { + const terms = { firstName: 'A', lastName: 'B' }; + service.advanceSearch(terms).subscribe(); + + const req = httpMock.expectOne(environment.advanceSearchUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(terms); + req.flush({}); + }); + }); + + describe('advanceSearchIdentity', () => { + it('should POST search terms to advanceSearchIdentityUrl', () => { + const terms = { firstName: 'C' }; + service.advanceSearchIdentity(terms).subscribe(); + + const req = httpMock.expectOne(environment.advanceSearchIdentityUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(terms); + req.flush({}); + }); + }); + + describe('loadMasterData', () => { + it('should POST spID to registrarMasterDataUrl', () => { + service.loadMasterData(55).subscribe(); + + const req = httpMock.expectOne(environment.registrarMasterDataUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ spID: 55 }); + req.flush({}); + }); + }); + + describe('patientRevisit', () => { + it('should POST benRegID to patientRevisitSubmitToNurse', () => { + const benRegID = { benRegID: 99 }; + service.patientRevisit(benRegID).subscribe(); + + const req = httpMock.expectOne(environment.patientRevisitSubmitToNurse); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(benRegID); + req.flush({}); + }); + }); + + describe('identityPatientRevisit', () => { + it('should POST ben data to identityPatientRevisitSubmitToNurseURL', () => { + const ben = { beneficiaryRegID: 77 }; + service.identityPatientRevisit(ben).subscribe(); + + const req = httpMock.expectOne( + environment.identityPatientRevisitSubmitToNurseURL + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(ben); + req.flush({}); + }); + }); + + describe('updatePatientData', () => { + it('should POST beneficiary to updateBeneficiaryUrl', () => { + const beneficiary = { id: 1, name: 'Updated' }; + service.updatePatientData(beneficiary).subscribe(); + + const req = httpMock.expectOne(environment.updateBeneficiaryUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(beneficiary); + req.flush({}); + }); + }); + + describe('getDistrictBlocks', () => { + it('should POST servicePointID to servicePointVillages', () => { + service.getDistrictBlocks(42).subscribe(); + + const req = httpMock.expectOne(environment.servicePointVillages); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ servicePointID: 42 }); + req.flush({}); + }); + }); + + describe('submitBeneficiary', () => { + it('should POST iEMRForm to submitBeneficiaryIdentityUrl', () => { + const form = { field: 'value' }; + service.submitBeneficiary(form).subscribe(); + + const req = httpMock.expectOne(environment.submitBeneficiaryIdentityUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(form); + req.flush({}); + }); + }); + + describe('updateBeneficiary', () => { + it('should POST iEMRForm to updateBeneficiaryIdentityUrl', () => { + const form = { field: 'updated' }; + service.updateBeneficiary(form).subscribe(); + + const req = httpMock.expectOne(environment.updateBeneficiaryIdentityUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(form); + req.flush({}); + }); + }); + + describe('getVillageList', () => { + it('should GET village list by blockId', () => { + const blockId = 10; + service.getVillageList(blockId).subscribe(); + + const req = httpMock.expectOne( + `${environment.getVillageListUrl}${blockId}` + ); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + describe('getSubDistrictList', () => { + it('should GET sub-district list by districtId', () => { + const districtId = 20; + service.getSubDistrictList(districtId).subscribe(); + + const req = httpMock.expectOne( + `${environment.getSubDistrictListUrl}${districtId}` + ); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + describe('getDistrictList', () => { + it('should GET district list by stateId', () => { + const stateId = 5; + service.getDistrictList(stateId).subscribe(); + + const req = httpMock.expectOne( + `${environment.getDistrictListUrl}${stateId}` + ); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + describe('getDistrictTalukList', () => { + it('should GET district taluk list by villageID', () => { + const villageID = 30; + service.getDistrictTalukList(villageID).subscribe(); + + const req = httpMock.expectOne( + `${environment.getDistrictTalukUrl}${villageID}` + ); + expect(req.request.method).toBe('GET'); + req.flush({}); + }); + }); + + describe('clearBeneficiaryEditDetails', () => { + it('should reset beneficiaryEditDetails to null', done => { + service.saveBeneficiaryEditDataASobservable({ name: 'Test' }); + service.clearBeneficiaryEditDetails(); + + service.beneficiaryEditDetails$.subscribe(val => { + expect(val).toBeNull(); + done(); + }); + }); + }); + + describe('saveBeneficiaryEditDataASobservable', () => { + it('should emit the given beneficiary on beneficiaryEditDetails$', done => { + const beneficiary = { firstName: 'X', lastName: 'Y' }; + service.saveBeneficiaryEditDataASobservable(beneficiary); + + service.beneficiaryEditDetails$.subscribe(val => { + expect(val).toEqual(beneficiary); + done(); + }); + }); + }); +}); diff --git a/src/app/app-modules/registrar/shared/utility/registration-utility.spec.ts b/src/app/app-modules/registrar/shared/utility/registration-utility.spec.ts new file mode 100644 index 00000000..187c450b --- /dev/null +++ b/src/app/app-modules/registrar/shared/utility/registration-utility.spec.ts @@ -0,0 +1,97 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { RegistrationUtils } from './registration-utility'; + +describe('RegistrationUtils', () => { + let utils: RegistrationUtils; + + beforeEach(() => { + utils = new RegistrationUtils(new FormBuilder()); + }); + + it('createRegistrationDetailsForm builds the three sub forms', () => { + const form = utils.createRegistrationDetailsForm(); + expect(form instanceof FormGroup).toBeTrue(); + expect(Object.keys(form.controls)).toEqual([ + 'personalDetailsForm', + 'demographicDetailsForm', + 'otherDetailsForm', + ]); + // stateID is required so a fresh form is invalid + expect(form.valid).toBeFalse(); + }); + + it('createPersonalDetailsForm has expected defaults', () => { + const form = utils.createPersonalDetailsForm(); + expect(form.value.beneficiaryRegID).toBe(0); + expect(form.value.imageChangeFlag).toBeFalse(); + expect(form.value.checked).toBeTrue(); + expect(form.value.firstName).toBeNull(); + expect(form.contains('monthlyFamilyIncome')).toBeTrue(); + expect(form.valid).toBeTrue(); + }); + + it('createDemographicDetailsForm has required stateID, default country and one village', () => { + const form = utils.createDemographicDetailsForm(); + expect(form.value.countryID).toBe(1); + expect(form.get('stateID')?.hasError('required')).toBeTrue(); + form.patchValue({ stateID: 5 } as any); + expect(form.valid).toBeTrue(); + const villages = form.get('villages') as FormArray; + expect(villages.length).toBe(1); + expect(villages.at(0).value).toEqual({ villageID: null }); + }); + + it('initVillage returns a group with villageID', () => { + const g = utils.initVillage(); + expect(g.value).toEqual({ villageID: null }); + }); + + it('createOtherDetailsForm contains govID and otherGovID arrays', () => { + const form = utils.createOtherDetailsForm(); + expect((form.get('govID') as FormArray).length).toBe(1); + expect((form.get('otherGovID') as FormArray).length).toBe(1); + expect(form.value.fatherName).toBeNull(); + expect(form.contains('religionOther')).toBeTrue(); + }); + + it('initGovID returns a group with all ID fields null', () => { + const g = utils.initGovID(); + expect(Object.keys(g.controls).sort()).toEqual( + [ + 'type', + 'pattern', + 'error', + 'allow', + 'benIdentityId', + 'deleted', + 'minLength', + 'maxLength', + 'idValue', + 'createdBy', + ].sort() + ); + Object.values(g.value).forEach(v => expect(v).toBeNull()); + }); +}); diff --git a/src/app/app-modules/reset-password/reset-password.component.spec.ts b/src/app/app-modules/reset-password/reset-password.component.spec.ts index b471ec0a..a887fcc2 100644 --- a/src/app/app-modules/reset-password/reset-password.component.spec.ts +++ b/src/app/app-modules/reset-password/reset-password.component.spec.ts @@ -19,28 +19,258 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + flushMicrotasks, +} from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + commonTestProviders, + autoSpy, + throwingObs, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; import { ResetPasswordComponent } from './reset-password.component'; +import { AuthService } from 'src/app/app-modules/core/services/auth.service'; +import { ConfirmationService } from '../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +const QUESTIONS = [ + { questionId: 1, question: 'Q1?' }, + { questionId: 2, question: 'Q2?' }, + { questionId: 3, question: 'Q3?' }, +]; describe('ResetPasswordComponent', () => { let component: ResetPasswordComponent; let fixture: ComponentFixture; + let auth: any; + let confirmation: any; + let session: any; + let router: Router; + let navigateSpy: jasmine.Spy; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + sessionStorage.clear(); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ResetPasswordComponent], + providers: [ + ...commonTestProviders(), + { provide: AuthService, useValue: autoSpy(AuthService) }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + auth = TestBed.inject(AuthService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + session = TestBed.inject(SessionStorageService) as any; + router = TestBed.inject(Router); + navigateSpy = spyOn(router, 'navigate').and.resolveTo(true); fixture = TestBed.createComponent(ResetPasswordComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { + afterEach(() => sessionStorage.clear()); + + it('creates with initial state', () => { expect(component).toBeTruthy(); + expect(component.showQuestions).toBeFalse(); + expect(component.hideOnGettingQuestions).toBeTrue(); + expect(component.counter).toBe(0); + expect(component.dynamictype).toBe('password'); + }); + + it('toggles password visibility', () => { + component.showPWD(); + expect(component.dynamictype).toBe('text'); + component.hidePWD(); + expect(component.dynamictype).toBe('password'); + }); + + describe('getQuestions', () => { + it('stores username and shows the first question on success', () => { + auth.getUserSecurityQuestionsAnswer.and.returnValue( + of({ data: { SecurityQuesAns: QUESTIONS } }) + ); + component.getQuestions('nurse1'); + expect(session.setItem).toHaveBeenCalledWith('userName', 'nurse1'); + expect(auth.getUserSecurityQuestionsAnswer).toHaveBeenCalledWith( + 'nurse1' + ); + expect(component.showQuestions).toBeTrue(); + expect(component.hideOnGettingQuestions).toBeFalse(); + expect(component.questions).toEqual(['Q1?', 'Q2?', 'Q3?']); + expect(component.questionId).toEqual([1, 2, 3]); + expect(component.bufferQuestion).toBe('Q1?'); + expect(component.bufferQuestionId).toBe(1); + }); + + it('ignores null response', () => { + spyOn(component, 'handleSuccess'); + auth.getUserSecurityQuestionsAnswer.and.returnValue(of(null)); + component.getQuestions('u'); + expect(component.handleSuccess).not.toHaveBeenCalled(); + }); + + it('stores the error on failure', () => { + auth.getUserSecurityQuestionsAnswer.and.returnValue( + throwingObs({ status: 500 }) + ); + component.getQuestions('u'); + expect(component.error).toEqual({ status: 500 }); + }); + }); + + describe('handleSuccess', () => { + it('logs out and alerts when no questions set', () => { + spyOn(component, 'logout'); + component.handleSuccess({ SecurityQuesAns: [] }); + expect(component.logout).toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Questions are not set', + 'error' + ); + }); + + it('logs out when user not found or data missing', () => { + spyOn(component, 'logout'); + component.handleSuccess({ forgetPassword: 'user Not Found' }); + component.handleSuccess(null); + component.handleSuccess(undefined); + expect(component.logout).toHaveBeenCalledTimes(3); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + }); + + describe('nextQuestion', () => { + beforeEach(() => { + component.handleSuccess({ SecurityQuesAns: QUESTIONS }); + }); + + it('collects answers and moves to the next question', () => { + component.answer = 'a1'; + component.wrong_answer_msg = 'old'; + component.nextQuestion(); + expect(component.userFinalAnswers).toEqual([ + { questionId: 1, answer: 'a1' }, + ]); + expect(component.counter).toBe(1); + expect(component.bufferQuestion).toBe('Q2?'); + expect(component.answer).toBeUndefined(); + expect(component.wrong_answer_msg).toBe(''); + }); + + it('validates answers after the third question', () => { + spyOn(component, 'checking'); + component.nextQuestion(); + component.nextQuestion(); + component.nextQuestion(); + expect(component.counter).toBe(3); + expect(component.checking).toHaveBeenCalledTimes(1); + expect(component.userFinalAnswers.length).toBe(3); + }); + + it('does nothing once counter reached 3', () => { + component.counter = 3; + component.nextQuestion(); + expect(component.userFinalAnswers.length).toBe(0); + expect(component.counter).toBe(3); + }); + }); + + describe('checking', () => { + beforeEach(() => { + session.store.set('userName', 'nurse1'); + component.handleSuccess({ SecurityQuesAns: QUESTIONS }); + component.userFinalAnswers = [{ questionId: 1, answer: 'a' }]; + component.answer = 'x'; + }); + + it('navigates to set-password and stores transaction id on success', () => { + auth.validateSecurityQuestionAndAnswer.and.returnValue( + of({ statusCode: 200, data: { transactionId: 'tx1' } }) + ); + const answers = component.userFinalAnswers; + component.counter = 3; + component.checking(); + expect(auth.validateSecurityQuestionAndAnswer).toHaveBeenCalledWith( + answers, + 'nurse1' + ); + expect(component.counter).toBe(0); + expect(router.navigate).toHaveBeenCalledWith(['/set-password']); + expect(auth.transactionId).toBe('tx1'); + expect(component.answer).toBeUndefined(); + expect(component.userFinalAnswers).toEqual([]); + }); + + it('alerts, refetches questions and stays on reset-password on wrong answers', () => { + auth.validateSecurityQuestionAndAnswer.and.returnValue( + of({ statusCode: 5002, errorMessage: 'Wrong answers' }) + ); + auth.getUserSecurityQuestionsAnswer.and.returnValue(of(null)); + component.counter = 3; + component.checking(); + expect(confirmation.alert).toHaveBeenCalledWith('Wrong answers', 'error'); + expect(auth.getUserSecurityQuestionsAnswer).toHaveBeenCalledWith( + 'nurse1' + ); + expect(router.navigate).toHaveBeenCalledWith(['/reset-password']); + expect(component.counter).toBe(0); + expect(component.showQuestions).toBeTrue(); + // current behaviour: question arrays are appended to, not reset + expect(component.questions.length).toBe(6); + }); + + it('handles null response without action', () => { + auth.validateSecurityQuestionAndAnswer.and.returnValue(of(null)); + component.checking(); + expect(router.navigate).not.toHaveBeenCalled(); + expect(component.userFinalAnswers).toEqual([]); + }); + + it('alerts and stays on reset-password on error', () => { + auth.validateSecurityQuestionAndAnswer.and.returnValue( + throwingObs({ errorMessage: 'boom' }) + ); + component.counter = 2; + component.checking(); + expect(confirmation.alert).toHaveBeenCalledWith('boom', 'error'); + expect(router.navigate).toHaveBeenCalledWith(['/reset-password']); + expect(component.counter).toBe(0); + }); + }); + + describe('logout', () => { + it('clears sessionStorage after navigating to login', fakeAsync(() => { + sessionStorage.setItem('k', 'v'); + component.logout(); + flushMicrotasks(); + expect(auth.logout).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/login']); + expect(sessionStorage.getItem('k')).toBeNull(); + })); + + it('keeps sessionStorage when navigation fails', fakeAsync(() => { + navigateSpy.and.resolveTo(false); + sessionStorage.setItem('k', 'v'); + component.logout(); + flushMicrotasks(); + expect(sessionStorage.getItem('k')).toBe('v'); + })); + }); + + it('renders the question section after questions are fetched', () => { + auth.getUserSecurityQuestionsAnswer.and.returnValue( + of({ data: { SecurityQuesAns: QUESTIONS } }) + ); + component.getQuestions('nurse1'); + fixture.detectChanges(); + expect(fixture.nativeElement.textContent).toContain('Q1?'); }); }); diff --git a/src/app/app-modules/service-point/service-point-resolve.service.spec.ts b/src/app/app-modules/service-point/service-point-resolve.service.spec.ts new file mode 100644 index 00000000..7f29d029 --- /dev/null +++ b/src/app/app-modules/service-point/service-point-resolve.service.spec.ts @@ -0,0 +1,194 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { TestBed } from '@angular/core/testing'; +import { Router, ActivatedRouteSnapshot } from '@angular/router'; +import { of, EMPTY } from 'rxjs'; +import { ServicePointResolve } from './service-point-resolve.service'; +import { ServicePointService } from './service-point.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; + +describe('ServicePointResolve', () => { + let service: ServicePointResolve; + let mockServicePointService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + let mockRoute: ActivatedRouteSnapshot; + + beforeEach(() => { + mockServicePointService = jasmine.createSpyObj('ServicePointService', [ + 'getServicePoints', + ]); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + mockRoute = {} as ActivatedRouteSnapshot; + + TestBed.configureTestingModule({ + providers: [ + ServicePointResolve, + { provide: ServicePointService, useValue: mockServicePointService }, + { provide: Router, useValue: mockRouter }, + { + provide: SessionStorageService, + useValue: mockSessionStorageService, + }, + ], + }); + service = TestBed.inject(ServicePointResolve); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#resolve', () => { + it('should get providerServiceID from session storage', () => { + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue( + of({ data: [] }) + ); + + service.resolve(mockRoute); + + expect(mockSessionStorageService.getItem).toHaveBeenCalledWith( + 'providerServiceID' + ); + }); + + it('should call getServicePoints with the providerServiceID', () => { + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue( + of({ data: [] }) + ); + + service.resolve(mockRoute); + + expect(mockServicePointService.getServicePoints).toHaveBeenCalledWith( + 'provider123' + ); + }); + + it('should return the response when it is truthy', (done: DoneFn) => { + const mockResponse = { + data: [{ servicePointID: 1, servicePointName: 'SP1' }], + }; + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue( + of(mockResponse) + ); + + service.resolve(mockRoute).subscribe(result => { + expect(result).toEqual(mockResponse); + done(); + }); + }); + + it('should navigate to /service when response is falsy (null)', (done: DoneFn) => { + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue(of(null as any)); + + service.resolve(mockRoute).subscribe(result => { + expect(result).toBeNull(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/service']); + done(); + }); + }); + + it('should navigate to /service when response is falsy (undefined)', (done: DoneFn) => { + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue( + of(undefined as any) + ); + + service.resolve(mockRoute).subscribe(result => { + expect(result).toBeNull(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/service']); + done(); + }); + }); + + it('should navigate to /service when response is empty string', (done: DoneFn) => { + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue(of('' as any)); + + service.resolve(mockRoute).subscribe(result => { + expect(result).toBeNull(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/service']); + done(); + }); + }); + + it('should not navigate when response is truthy', (done: DoneFn) => { + const mockResponse = { data: 'test' }; + mockSessionStorageService.getItem.and.returnValue('provider123'); + mockServicePointService.getServicePoints.and.returnValue( + of(mockResponse) + ); + + service.resolve(mockRoute).subscribe(() => { + expect(mockRouter.navigate).not.toHaveBeenCalled(); + done(); + }); + }); + + it('should handle null providerServiceID from session storage', () => { + mockSessionStorageService.getItem.and.returnValue(null); + mockServicePointService.getServicePoints.and.returnValue( + of({ data: [] }) + ); + + service.resolve(mockRoute).subscribe(); + + expect(mockServicePointService.getServicePoints).toHaveBeenCalledWith( + null as any + ); + }); + + it('should return response with service point data', (done: DoneFn) => { + const mockResponse = { + data: [ + { + servicePointID: 1, + servicePointName: 'Service Point 1', + vanID: 100, + }, + { + servicePointID: 2, + servicePointName: 'Service Point 2', + vanID: 101, + }, + ], + }; + mockSessionStorageService.getItem.and.returnValue('providerXYZ'); + mockServicePointService.getServicePoints.and.returnValue( + of(mockResponse) + ); + + service.resolve(mockRoute).subscribe(result => { + expect(result).toEqual(mockResponse); + expect(result.data.length).toBe(2); + done(); + }); + }); + }); +}); diff --git a/src/app/app-modules/service-point/service-point.component.spec.ts b/src/app/app-modules/service-point/service-point.component.spec.ts new file mode 100644 index 00000000..47e89b5d --- /dev/null +++ b/src/app/app-modules/service-point/service-point.component.spec.ts @@ -0,0 +1,530 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + commonTestProviders, + autoSpy, + throwingObs, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; +import { ServicePointComponent } from './service-point.component'; +import { ServicePointService } from './service-point.service'; +import { RegistrarService } from '../registrar/shared/services/registrar.service'; +import { SetLanguageComponent } from '../core/components/set-language.component'; +import { ConfirmationService } from '../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; + +const VAN_SP = [ + { + vanID: 1, + vanSession: 1, + vanNoAndType: 'KA01 - Mobile', + servicePointID: 10, + servicePointName: 'Alpha', + facilityID: 99, + }, + { + vanID: 1, + vanSession: 1, + vanNoAndType: 'KA01 - Mobile', + servicePointID: 10, + servicePointName: 'Alpha', + facilityID: 99, + }, + { + vanID: 1, + vanSession: 1, + vanNoAndType: 'KA01 - Mobile', + servicePointID: 11, + servicePointName: 'Beta', + facilityID: 99, + }, + { + vanID: 2, + vanSession: 3, + vanNoAndType: 'KA02', + servicePointID: 20, + servicePointName: 'Gamma', + }, + { + vanID: 2, + vanSession: '3', + vanNoAndType: 'KA02', + servicePointID: 21, + servicePointName: 'Delta', + }, + { + vanID: 3, + vanSession: 2, + vanNoAndType: 'KA03 - Bus', + servicePointID: 30, + servicePointName: 'Eps', + }, +]; + +describe('ServicePointComponent', () => { + let component: ServicePointComponent; + let fixture: ComponentFixture; + let routeData: BehaviorSubject; + let spService: any; + let registrar: any; + let confirmation: any; + let session: any; + let tracking: any; + let router: Router; + + async function setup(data: any) { + routeData = new BehaviorSubject(data); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], + declarations: [ServicePointComponent], + providers: [ + ...commonTestProviders({ + session: { + providerServiceID: 101, + userID: 7, + designation: 'Nurse', + }, + }), + { provide: ActivatedRoute, useValue: { data: routeData } }, + { + provide: ServicePointService, + useValue: autoSpy(ServicePointService), + }, + { provide: RegistrarService, useValue: autoSpy(RegistrarService) }, + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideTemplate(ServicePointComponent, '') + .compileComponents(); + spService = TestBed.inject(ServicePointService) as any; + registrar = TestBed.inject(RegistrarService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + session = TestBed.inject(SessionStorageService) as any; + tracking = TestBed.inject(AmritTrackingService) as any; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); + fixture = TestBed.createComponent(ServicePointComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + } + + const ok = (d: any = { UserVanSpDetails: VAN_SP }) => ({ + servicePoints: { statusCode: 200, data: d }, + }); + + describe('init / getServicePoint', () => { + it('loads van service point details from the resolver', async () => { + await setup(ok()); + expect(component.serviceProviderId).toBe(101); + expect(component.userId).toBe(7); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + expect(component.vanServicepointDetails).toBe(VAN_SP); + }); + + it('ignores 200 without UserVanSpDetails', async () => { + await setup(ok({})); + expect(component.vanServicepointDetails).toBeUndefined(); + expect(confirmation.alert).not.toHaveBeenCalled(); + }); + + it('alerts on 200 with null data and returns to service', async () => { + await setup({ + servicePoints: { statusCode: 200, data: null, errorMessage: 'none' }, + }); + expect(confirmation.alert).toHaveBeenCalledWith('none', 'error'); + expect(router.navigate).toHaveBeenCalledWith(['/service']); + }); + + it('alerts on 5002 without navigating', async () => { + await setup({ + servicePoints: { statusCode: 5002, errorMessage: 'session' }, + }); + expect(confirmation.alert).toHaveBeenCalledWith('session', 'error'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts on route data error', async () => { + await setup(ok()); + (TestBed.inject(ActivatedRoute) as any).data = throwingObs('bad'); + component.getServicePoint(); + expect(confirmation.alert).toHaveBeenCalledWith('bad', 'error'); + }); + + it('ngDoCheck refreshes language', async () => { + await setup(ok()); + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + }); + }); + + describe('van / service point filtering', () => { + beforeEach(async () => { + await setup(ok()); + }); + + it('filterVansList keeps matching session plus full-day vans, de-duplicated', () => { + component.servicePointForm.controls.sessionID.setValue(1 as any); + component.servicePointForm.controls.vanID.setValue(5 as any); + component.filterVansList(); + expect(session.setItem).toHaveBeenCalledWith('sessionID', 1); + expect(component.vansList.map((v: any) => v.vanID)).toEqual([1, 2]); + expect(component.servicePointForm.controls.vanID.value).toBeNull(); + expect(component.servicePointsList).toEqual([]); + }); + + it('filterVansList without session or details', () => { + component.vanServicepointDetails = undefined; + component.filterVansList(); + expect(session.setItem).not.toHaveBeenCalledWith( + 'sessionID', + jasmine.anything() + ); + expect(component.vansList).toEqual([]); + }); + + it('filterServicePointsList filters by van and session, stores details', () => { + component.servicePointForm.controls.sessionID.setValue(1 as any); + component.filterVansList(); + component.servicePointForm.controls.vanID.setValue(1 as any); + component.filterServicePointsList(); + expect(session.setItem).toHaveBeenCalledWith('vanType', 'Mobile'); + expect(session.setItem).toHaveBeenCalledWith( + 'serviceLineDetails', + JSON.stringify(VAN_SP[0]) + ); + expect(session.setItem).toHaveBeenCalledWith('facilityID', 99); + expect(component.isDisabled).toBeFalse(); + expect( + component.servicePointsList.map((s: any) => s.servicePointID) + ).toEqual([10, 11]); + expect(component.filteredServicePoints.length).toBe(2); + }); + + it('filterServicePointsList handles string full-day session and no facility', () => { + component.servicePointForm.controls.sessionID.setValue(1 as any); + component.filterVansList(); + component.servicePointForm.controls.vanID.setValue(2 as any); + component.filterServicePointsList(); + expect(session.setItem).not.toHaveBeenCalledWith( + 'facilityID', + jasmine.anything() + ); + expect(session.setItem).not.toHaveBeenCalledWith( + 'vanType', + jasmine.anything() + ); + expect( + component.servicePointsList.map((s: any) => s.servicePointID) + ).toEqual([21]); + }); + + it('filterServicePointsList with no details leaves list empty', () => { + component.vansList = [{ vanID: 4, vanNoAndType: 'X - Y' }]; + component.vanServicepointDetails = undefined; + component.servicePointsList = []; + component.servicePointForm.controls.vanID.setValue(4 as any); + component.filterServicePointsList(); + expect(component.filteredServicePoints).toEqual([]); + }); + + it('filterServicePointVan filters by name prefix or resets', () => { + component.servicePointsList = VAN_SP.slice(2); + component.filterServicePointVan('ga'); + expect( + component.filteredServicePoints.map((s: any) => s.servicePointName) + ).toEqual(['Gamma']); + component.filterServicePointVan(''); + expect(component.filteredServicePoints.length).toBe(4); + }); + }); + + describe('routeToDesignation', () => { + beforeEach(async () => { + await setup(ok()); + }); + + const cases: [string, string][] = [ + ['Registrar', '/registrar/search'], + ['Nurse', '/nurse-doctor/nurse-worklist'], + ['Doctor', '/nurse-doctor/doctor-worklist'], + ['Lab Technician', '/lab'], + ['Pharmacist', '/pharmacist'], + ['Radiologist', '/nurse-doctor/radiologist-worklist'], + ['Oncologist', '/nurse-doctor/oncologist-worklist'], + ]; + cases.forEach(([d, url]) => + it(`routes ${d} to ${url}`, () => { + component.routeToDesignation(d); + expect(router.navigate).toHaveBeenCalledWith([url]); + }) + ); + + it('does not route unknown designations', () => { + component.routeToDesignation('Janitor'); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('goToWorkList uses stored designation', () => { + component.goToWorkList(); + expect(component.designation).toBe('Nurse'); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + }); + + describe('demographics', () => { + const userDetails = (districtList: any[]) => ({ + userDetails: { + stateID: 1, + stateName: 'Karnataka', + districtList, + blockList: [{ blockId: 55 }], + }, + stateMaster: [{ stateID: 1, stateName: 'Karnataka' }], + }); + + beforeEach(async () => { + await setup(ok()); + component.servicePointsList = VAN_SP.slice(2, 4); + }); + + it('getDemographics fetches and saves location for a matching service point', () => { + const data = userDetails([{ districtID: 5, districtName: 'Mysore' }]); + spService.getMMUDemographics.and.returnValue( + of({ statusCode: 200, data }) + ); + registrar.getSubDistrictList.and.returnValue( + of({ statusCode: 200, data: [{ blockID: 9 }] }) + ); + component.servicePointForm.controls.servicePointName.setValue('Beta'); + component.getDemographics(); + expect(session.setItem).toHaveBeenCalledWith('servicePointID', 11); + expect(session.setItem).toHaveBeenCalledWith('servicePointName', 'Beta'); + expect(spService.getMMUDemographics).toHaveBeenCalledWith(11, 101); + expect(session.setItem).toHaveBeenCalledWith( + 'location', + JSON.stringify(data) + ); + expect(component.statesList).toEqual(data.stateMaster); + expect(component.servicePointForm.controls.stateID.value).toBe(1 as any); + expect(component.servicePointForm.controls.districtID.value).toBe( + 5 as any + ); + expect(component.servicePointForm.controls.districtName.value).toBe( + 'Mysore' + ); + expect(registrar.getSubDistrictList).toHaveBeenCalledWith(5); + expect(component.subDistrictList).toEqual([{ blockID: 9 }]); + }); + + it('getDemographics alerts location issue on non-200', () => { + spService.getMMUDemographics.and.returnValue(of({ statusCode: 500 })); + component.servicePointForm.controls.servicePointName.setValue('Beta'); + component.getDemographics(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Issues in getting your location, Please try to re-login.', + 'error' + ); + }); + + it('getDemographics resets location when no service point matches', () => { + component.statesList = [1]; + component.servicePointForm.controls.stateID.setValue(3 as any); + component.servicePointForm.controls.servicePointName.setValue('Nope'); + component.getDemographics(); + expect(spService.getMMUDemographics).not.toHaveBeenCalled(); + expect(component.statesList).toEqual([]); + expect(component.servicePointForm.controls.stateID.value).toBeNull(); + }); + + it('saveDemographicsToStorage keeps multiple districts without auto-select', () => { + const list = [{ districtID: 5 }, { districtID: 6 }]; + component.saveDemographicsToStorage(userDetails(list)); + expect(component.districtList).toBe(list); + expect(registrar.getSubDistrictList).not.toHaveBeenCalled(); + }); + + it('saveDemographicsToStorage alerts when userDetails missing', () => { + component.saveDemographicsToStorage({}); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingLocationDetails, + 'error' + ); + }); + + it('saveDemographicsToStorage alerts location issue when data missing', () => { + spyOn(component, 'locationGathetingIssues'); + component.saveDemographicsToStorage(null); + expect(component.locationGathetingIssues).toHaveBeenCalled(); + }); + }); + + describe('location masters', () => { + beforeEach(async () => { + await setup(ok()); + }); + + it('fetchDistricts success and failure', () => { + registrar.getDistrictList.and.returnValue( + of({ statusCode: 200, data: [1] }) + ); + component.fetchDistricts(1); + expect(registrar.getDistrictList).toHaveBeenCalledWith(1); + expect(component.districtList).toEqual([1]); + registrar.getDistrictList.and.returnValue(of({ statusCode: 500 })); + component.fetchDistricts(1); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + }); + + it('fetchDistrictsOnStateSelection sets state name and districts', () => { + component.statesList = [ + { stateID: 1, stateName: 'A' }, + { stateID: 2, stateName: 'B' }, + ]; + component.servicePointForm.controls.blockID.setValue(4 as any); + registrar.getDistrictList.and.returnValue( + of({ statusCode: 200, data: [9] }) + ); + component.fetchDistrictsOnStateSelection(2); + expect(component.servicePointForm.controls.stateName.value).toBe('B'); + expect(component.districtList).toEqual([9]); + expect(component.servicePointForm.controls.blockID.value).toBeNull(); + }); + + it('fetchDistrictsOnStateSelection alerts on failure without state', () => { + registrar.getDistrictList.and.returnValue(of(null)); + component.fetchDistrictsOnStateSelection(null); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesInFetchingDemographics, + 'error' + ); + }); + + it('fetchSubDistrictsOnDistrictSelection sets district name; ignores failure', () => { + component.districtList = [ + { districtID: 5, districtName: 'M' }, + { districtID: 6, districtName: 'N' }, + ]; + registrar.getSubDistrictList.and.returnValue(of({ statusCode: 500 })); + component.fetchSubDistrictsOnDistrictSelection(6); + expect(component.servicePointForm.controls.districtName.value).toBe('N'); + expect(component.subDistrictList).toEqual([]); + component.fetchSubDistrictsOnDistrictSelection(null); + expect(registrar.getSubDistrictList).toHaveBeenCalledWith(null); + }); + + it('getVillageMaster loads villages or alerts', () => { + registrar.getVillageList.and.returnValue( + of({ statusCode: 200, data: [1, 2] }) + ); + component.getVillageMaster({ + userDetails: { blockList: [{ blockId: 55 }] }, + }); + expect(registrar.getVillageList).toHaveBeenCalledWith(55); + expect(component.villageList).toEqual([1, 2]); + registrar.getVillageList.and.returnValue(of({ statusCode: 500 })); + component.getVillageMaster(null); + expect(registrar.getVillageList).toHaveBeenCalledWith(undefined); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesinfetchingLocation, + 'error' + ); + }); + + it('onSubDistrictChange sets block name and villages', () => { + component.subDistrictList = [ + { blockID: 3, blockName: 'Blk' }, + { blockID: 4, blockName: 'Other' }, + ]; + registrar.getVillageList.and.returnValue( + of({ statusCode: 200, data: [7] }) + ); + component.onSubDistrictChange(3); + expect(component.servicePointForm.controls.blockName.value).toBe('Blk'); + expect(component.villageList).toEqual([7]); + registrar.getVillageList.and.returnValue(of({ statusCode: 500 })); + component.onSubDistrictChange(null); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.issuesinfetchingLocation, + 'error' + ); + }); + + it('onDistrictBranchSelection sets village name', () => { + component.villageList = [ + { districtBranchID: 1, villageName: 'V1' }, + { districtBranchID: 2, villageName: 'V2' }, + ]; + component.onDistrictBranchSelection(2); + expect(component.servicePointForm.controls.villageName.value).toBe('V2'); + component.onDistrictBranchSelection(null); + expect(component.servicePointForm.controls.villageName.value).toBe('V2'); + }); + + it('saveLocationDataToStorage stores the form location and routes', () => { + component.servicePointForm.patchValue({ + stateID: 1 as any, + stateName: 'S', + districtID: 2 as any, + districtName: 'D', + blockID: 3 as any, + blockName: 'B', + districtBranchID: 4 as any, + villageName: 'V', + }); + component.saveLocationDataToStorage(); + expect(JSON.parse(session.store.get('locationData'))).toEqual({ + stateID: 1, + stateName: 'S', + districtID: 2, + districtName: 'D', + blockName: 'B', + blockID: 3, + subDistrictID: 4, + villageName: 'V', + }); + expect(router.navigate).toHaveBeenCalledWith([ + '/nurse-doctor/nurse-worklist', + ]); + }); + + it('trackFieldInteraction forwards to tracking service', () => { + component.trackFieldInteraction('Van'); + expect(tracking.trackFieldInteraction).toHaveBeenCalledWith( + 'Van', + 'Service Point' + ); + }); + }); +}); diff --git a/src/app/app-modules/service-point/service-point.service.spec.ts b/src/app/app-modules/service-point/service-point.service.spec.ts new file mode 100644 index 00000000..09ac5b36 --- /dev/null +++ b/src/app/app-modules/service-point/service-point.service.spec.ts @@ -0,0 +1,172 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +import { TestBed } from '@angular/core/testing'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { Router } from '@angular/router'; +import { ServicePointService } from './service-point.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { environment } from 'src/environments/environment'; + +describe('ServicePointService', () => { + let service: ServicePointService; + let httpMock: HttpTestingController; + let mockRouter: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + + beforeEach(() => { + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + ]); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + ServicePointService, + { provide: Router, useValue: mockRouter }, + { + provide: SessionStorageService, + useValue: mockSessionStorageService, + }, + ], + }); + service = TestBed.inject(ServicePointService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('#getServicePoints', () => { + it('should make a POST request to servicePointUrl', () => { + const serviceProviderId = 'provider123'; + const mockResponse = { + data: [{ servicePointID: 1, servicePointName: 'SP1' }], + }; + + service.getServicePoints(serviceProviderId).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(environment.servicePointUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + providerServiceMapID: serviceProviderId, + }); + req.flush(mockResponse); + }); + + it('should send providerServiceMapID in request body', () => { + const serviceProviderId = 'testId456'; + + service.getServicePoints(serviceProviderId).subscribe(); + + const req = httpMock.expectOne(environment.servicePointUrl); + expect(req.request.body.providerServiceMapID).toBe(serviceProviderId); + req.flush({}); + }); + + it('should handle empty response', () => { + service.getServicePoints('id1').subscribe(response => { + expect(response).toEqual({}); + }); + + const req = httpMock.expectOne(environment.servicePointUrl); + req.flush({}); + }); + + it('should handle HTTP error', () => { + service.getServicePoints('id1').subscribe({ + error: error => { + expect(error.status).toBe(500); + }, + }); + + const req = httpMock.expectOne(environment.servicePointUrl); + req.flush('Server Error', { + status: 500, + statusText: 'Internal Server Error', + }); + }); + }); + + describe('#getMMUDemographics', () => { + it('should make a POST request to demographicsCurrentMasterUrl', () => { + const spID = '10'; + const spPSMID = '20'; + const mockResponse = { + data: { stateID: 1, districtID: 2 }, + }; + + service.getMMUDemographics(spID, spPSMID).subscribe(response => { + expect(response).toEqual(mockResponse); + }); + + const req = httpMock.expectOne(environment.demographicsCurrentMasterUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + spID: spID, + spPSMID: spPSMID, + }); + req.flush(mockResponse); + }); + + it('should send correct parameters in request body', () => { + const spID = 'servicePoint1'; + const spPSMID = 'psmId1'; + + service.getMMUDemographics(spID, spPSMID).subscribe(); + + const req = httpMock.expectOne(environment.demographicsCurrentMasterUrl); + expect(req.request.body.spID).toBe(spID); + expect(req.request.body.spPSMID).toBe(spPSMID); + req.flush({}); + }); + + it('should handle null parameters', () => { + service.getMMUDemographics(null, null).subscribe(); + + const req = httpMock.expectOne(environment.demographicsCurrentMasterUrl); + expect(req.request.body).toEqual({ spID: null, spPSMID: null }); + req.flush({}); + }); + + it('should handle HTTP error', () => { + service.getMMUDemographics('sp1', 'psm1').subscribe({ + error: error => { + expect(error.status).toBe(404); + }, + }); + + const req = httpMock.expectOne(environment.demographicsCurrentMasterUrl); + req.flush('Not Found', { status: 404, statusText: 'Not Found' }); + }); + }); +}); diff --git a/src/app/app-modules/service/service.component.spec.ts b/src/app/app-modules/service/service.component.spec.ts index d9303d05..ed05231a 100644 --- a/src/app/app-modules/service/service.component.spec.ts +++ b/src/app/app-modules/service/service.component.spec.ts @@ -19,28 +19,185 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { + COMMON_TEST_IMPORTS, + commonTestProviders, + LANGUAGE_EN, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; +import { ServiceComponent } from './service.component'; +import { SetLanguageComponent } from '../core/components/set-language.component'; +import { ConfirmationService } from '../core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +const SERVICES = [ + { + providerServiceID: 101, + serviceName: 'MMU', + serviceID: 2, + apimanClientKey: 'api-key', + }, +]; -import { ServiceComponent } from './service.component'; +function loginData(overrides: any = {}) { + return { + designation: { designationName: 'Nurse' }, + previlegeObj: [ + { + serviceName: 'MMU', + roles: [ + { + serviceRoleScreenMappings: [ + { screen: { screenName: 'Registrar' } }, + { screen: { screenName: 'Nurse' } }, + ], + }, + ], + }, + ], + ...overrides, + }; +} describe('ServiceComponent', () => { let component: ServiceComponent; let fixture: ComponentFixture; + let session: any; + let confirmation: any; + let router: Router; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + sessionStorage.clear(); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [ServiceComponent], + providers: [ + ...commonTestProviders({ + session: { + services: JSON.stringify(SERVICES), + fullName: 'Nurse One', + }, + }), + { provide: SetLanguageComponent, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + session = TestBed.inject(SessionStorageService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.resolveTo(true); fixture = TestBed.createComponent(ServiceComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + afterEach(() => sessionStorage.clear()); + + it('loads services, name and language on init and renders them', () => { + expect(component.servicesList).toEqual(SERVICES); + expect(component.fullName).toBe('Nurse One'); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + const text = fixture.nativeElement.textContent; + expect(text).toContain('Nurse One'); + expect(text).toContain('MMU'); + }); + + it('ngDoCheck refreshes the language set', () => { + component.currentLanguageSet = null; + component.ngDoCheck(); + expect(component.currentLanguageSet).toBe(LANGUAGE_EN); + }); + + describe('selectService', () => { + it('stores service details and routes to servicePoint when designation matches a role', () => { + session.store.set('loginDataResponse', JSON.stringify(loginData())); + fixture.nativeElement.querySelector('.circle').click(); + expect(session.setItem).toHaveBeenCalledWith('providerServiceID', 101); + expect(session.setItem).toHaveBeenCalledWith('serviceName', 'MMU'); + expect(session.setItem).toHaveBeenCalledWith('serviceID', 2); + expect(sessionStorage.getItem('apimanClientKey')).toBe('api-key'); + expect(session.setItem).toHaveBeenCalledWith( + 'role', + JSON.stringify(['Registrar', 'Nurse']) + ); + expect(session.setItem).toHaveBeenCalledWith('designation', 'Nurse'); + expect(router.navigate).toHaveBeenCalledWith(['/servicePoint']); + }); + + it('alerts when designation is not one of the roles', () => { + session.store.set( + 'loginDataResponse', + JSON.stringify( + loginData({ designation: { designationName: 'Doctor' } }) + ) + ); + component.selectService(SERVICES[0]); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mapDesignation, + 'error' + ); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('alerts when no designation is mapped', () => { + session.store.set( + 'loginDataResponse', + JSON.stringify(loginData({ designation: null })) + ); + component.selectService(SERVICES[0]); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mapDesignation, + 'error' + ); + }); + + it('does nothing when login data has no privileges', () => { + spyOn(component, 'checkMappedRoleForService'); + session.store.set( + 'loginDataResponse', + JSON.stringify(loginData({ previlegeObj: null })) + ); + component.selectService(SERVICES[0]); + component.checkRoleAndDesingnationMappedForservice(null, SERVICES[0]); + expect(component.checkMappedRoleForService).not.toHaveBeenCalled(); + }); + }); + + describe('checkMappedRoleForService', () => { + it('alerts when roles missing', () => { + component.checkMappedRoleForService({}); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mapRoleFeature, + 'error' + ); + }); + + it('alerts when roles empty', () => { + component.checkMappedRoleForService({ roles: [] }); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mapRoleFeature, + 'error' + ); + }); + + it('alerts when roles have no screen mappings', () => { + spyOn(component, 'checkMappedDesignation'); + component.checkMappedRoleForService({ + roles: [{ serviceRoleScreenMappings: [] }], + }); + expect(component.roleArray).toEqual([]); + expect(component.checkMappedDesignation).not.toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + LANGUAGE_EN.alerts.info.mapRoleFeature, + 'error' + ); + }); + }); + + it('routeToDesignation always navigates to servicePoint', () => { + component.routeToDesignation('Doctor'); + expect(router.navigate).toHaveBeenCalledWith(['/servicePoint']); }); }); diff --git a/src/app/app-modules/set-password/set-password.component.spec.ts b/src/app/app-modules/set-password/set-password.component.spec.ts index de46c4dd..461a95a4 100644 --- a/src/app/app-modules/set-password/set-password.component.spec.ts +++ b/src/app/app-modules/set-password/set-password.component.spec.ts @@ -19,28 +19,177 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { + ComponentFixture, + TestBed, + fakeAsync, + flushMicrotasks, +} from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { of } from 'rxjs'; +import { + COMMON_TEST_IMPORTS, + commonTestProviders, + autoSpy, + throwingObs, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; import { SetPasswordComponent } from './set-password.component'; +import { AuthService } from 'src/app/app-modules/core/services/auth.service'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; describe('SetPasswordComponent', () => { let component: SetPasswordComponent; let fixture: ComponentFixture; + let auth: any; + let confirmation: any; + let router: Router; + let navigateSpy: jasmine.Spy; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + sessionStorage.clear(); + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [SetPasswordComponent], + providers: [ + ...commonTestProviders({ session: { userName: 'nurse1' } }), + { + provide: AuthService, + useValue: autoSpy(AuthService, { transactionId: 'tx1' }), + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + auth = TestBed.inject(AuthService) as any; + confirmation = TestBed.inject(ConfirmationService) as any; + router = TestBed.inject(Router); + navigateSpy = spyOn(router, 'navigate').and.resolveTo(true); fixture = TestBed.createComponent(SetPasswordComponent); component = fixture.componentInstance; fixture.detectChanges(); + component.iterationCount = 1; }); - it('should be created', () => { + afterEach(() => sessionStorage.clear()); + + it('creates and reads the username from session storage', () => { expect(component).toBeTruthy(); + expect(component.uname).toBe('nurse1'); + expect(component._keySize).toBe(256); + expect(component._ivSize).toBe(128); + }); + + it('toggles password visibility', () => { + component.showPWD(); + expect(component.dynamictype).toBe('text'); + component.hidePWD(); + expect(component.dynamictype).toBe('password'); + }); + + it('keySize / iterationCount accessors', () => { + component.keySize = 128; + component.iterationCount = 3; + expect(component.keySize).toBe(128); + expect(component._iterationCount).toBe(3); + expect(component.iterationCount).toBe(3); + }); + + it('encrypt produces hex salt+iv prefix and is deterministic for fixed salt/iv', () => { + const out = component.encrypt(component.Key_IV, 'pwd'); + expect(/^[0-9a-f]{96}.+/.test(out)).toBeTrue(); + const a = component.encryptWithIvSalt( + 'aa'.repeat(32), + 'bb'.repeat(16), + 'k', + 'p' + ); + expect(a).toBe( + component.encryptWithIvSalt('aa'.repeat(32), 'bb'.repeat(16), 'k', 'p') + ); + expect(component.generateKey('aa'.repeat(32), 'k').sigBytes).toBe(32); + }); + + describe('updatePassword', () => { + it('alerts when passwords do not match', () => { + component.confirmpwd = 'other'; + component.updatePassword('pwd'); + expect(auth.setNewPassword).not.toHaveBeenCalled(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Password does not match', + 'error' + ); + }); + + it('sets the new password and logs out on success', () => { + spyOn(component, 'logout'); + component.confirmpwd = 'pwd'; + auth.setNewPassword.and.returnValue(of({ statusCode: 200 })); + component.updatePassword('pwd'); + const args = auth.setNewPassword.calls.mostRecent().args; + expect(args[0]).toBe('nurse1'); + expect(args[1]).toBe(component.password); + expect(args[2]).toBe('tx1'); + expect(component.encryptedConfirmPwd).toBeTruthy(); + // the "complete" argument is an assignment, evaluated eagerly + expect(auth.transactionId).toBeUndefined(); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Password changed successfully', + 'success' + ); + expect(component.logout).toHaveBeenCalled(); + }); + + it('alerts and returns to reset-password on non-200', () => { + component.confirmpwd = 'pwd'; + auth.setNewPassword.and.returnValue( + of({ statusCode: 5002, errorMessage: 'expired' }) + ); + component.updatePassword('pwd'); + expect(confirmation.alert).toHaveBeenCalledWith('expired', 'error'); + expect(router.navigate).toHaveBeenCalledWith(['/reset-password']); + }); + + it('alerts and returns to reset-password on error', () => { + component.confirmpwd = 'pwd'; + auth.setNewPassword.and.returnValue(throwingObs({ errorMessage: 'err' })); + component.updatePassword('pwd'); + expect(confirmation.alert).toHaveBeenCalledWith('err', 'error'); + expect(router.navigate).toHaveBeenCalledWith(['/reset-password']); + }); + }); + + describe('logout', () => { + it('clears sessionStorage after navigating to login', fakeAsync(() => { + sessionStorage.setItem('k', 'v'); + component.logout(); + flushMicrotasks(); + expect(auth.logout).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/login']); + expect(sessionStorage.getItem('k')).toBeNull(); + })); + + it('keeps sessionStorage if navigation is rejected', fakeAsync(() => { + navigateSpy.and.resolveTo(false); + sessionStorage.setItem('k', 'v'); + component.logout(); + flushMicrotasks(); + expect(sessionStorage.getItem('k')).toBe('v'); + })); + }); + + it('successCallback alerts success and logs out', () => { + spyOn(component, 'logout'); + component.successCallback({}); + expect(confirmation.alert).toHaveBeenCalledWith( + 'Password changed successfully', + 'success' + ); + expect(component.logout).toHaveBeenCalled(); + }); + + it('errorCallback logs the response', () => { + spyOn(console, 'log'); + component.errorCallback('bad'); + expect(console.log).toHaveBeenCalledWith('bad'); }); }); diff --git a/src/app/app-modules/set-security-questions/set-security-questions.component.spec.ts b/src/app/app-modules/set-security-questions/set-security-questions.component.spec.ts index 6c8b615e..af02834c 100644 --- a/src/app/app-modules/set-security-questions/set-security-questions.component.spec.ts +++ b/src/app/app-modules/set-security-questions/set-security-questions.component.spec.ts @@ -20,17 +20,79 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatIconModule } from '@angular/material/icon'; import { SetSecurityQuestionsComponent } from './set-security-questions.component'; +import { AuthService, ConfirmationService } from '../core/services'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; describe('SetSecurityQuestionsComponent', () => { let component: SetSecurityQuestionsComponent; let fixture: ComponentFixture; + let mockAuthService: jasmine.SpyObj; + let mockConfirmationService: jasmine.SpyObj; + let mockSessionStorageService: jasmine.SpyObj; + let mockRouter: jasmine.SpyObj; + + const mockQuestions = [ + { QuestionID: 1, Question: 'Favorite color?' }, + { QuestionID: 2, Question: 'Pet name?' }, + { QuestionID: 3, Question: 'City of birth?' }, + { QuestionID: 4, Question: 'Favorite food?' }, + ]; + + beforeEach(waitForAsync(() => { + mockAuthService = jasmine.createSpyObj('AuthService', [ + 'getSecurityQuestions', + 'saveUserSecurityQuestionsAnswer', + 'setNewPassword', + 'logout', + ]); + mockConfirmationService = jasmine.createSpyObj('ConfirmationService', [ + 'alert', + ]); + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', [ + 'getItem', + 'setItem', + ]); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + + mockAuthService.getSecurityQuestions.and.returnValue( + of({ data: mockQuestions }) + ); + mockSessionStorageService.getItem.and.callFake((key: string) => { + if (key === 'userID') return '456'; + if (key === 'userName') return 'testuser'; + return null; + }); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SetSecurityQuestionsComponent], + imports: [ + FormsModule, + RouterTestingModule, + NoopAnimationsModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatIconModule, + ], + providers: [ + { provide: AuthService, useValue: mockAuthService }, + { provide: ConfirmationService, useValue: mockConfirmationService }, + { provide: SessionStorageService, useValue: mockSessionStorageService }, + { provide: Router, useValue: mockRouter }, + ], }).compileComponents(); })); @@ -40,7 +102,423 @@ describe('SetSecurityQuestionsComponent', () => { fixture.detectChanges(); }); - it('should be created', () => { + it('should create', () => { expect(component).toBeTruthy(); }); + + describe('constructor', () => { + it('should set crypto defaults', () => { + expect(component['_keySize']).toBe(256); + expect(component['_ivSize']).toBe(128); + expect(component['_iterationCount']).toBe(1989); + }); + }); + + describe('ngOnInit', () => { + it('should load userID and userName from session storage', () => { + expect(component.uid).toBe('456'); + expect(component.uname).toBe('testuser'); + }); + + it('should call getSecurityQuestions', () => { + expect(mockAuthService.getSecurityQuestions).toHaveBeenCalled(); + }); + + it('should populate questions arrays on success', () => { + expect(component.questions).toEqual(mockQuestions); + expect(component.replica_questions).toEqual(mockQuestions); + expect(component.Q_array_one).toEqual(mockQuestions); + expect(component.Q_array_two).toEqual(mockQuestions); + }); + + it('should handle error from getSecurityQuestions', () => { + mockAuthService.getSecurityQuestions.and.returnValue( + throwError({ message: 'Error' }) + ); + component.ngOnInit(); + // handleError is a no-op, just verify no crash + expect(component).toBeTruthy(); + }); + }); + + describe('initial state', () => { + it('should have passwordSection false and questionsection true', () => { + expect(component.passwordSection).toBeFalse(); + expect(component.questionsection).toBeTrue(); + }); + + it('should have dynamictype as password', () => { + expect(component.dynamictype).toBe('password'); + }); + + it('should have empty selectedQuestions', () => { + expect(component.selectedQuestions).toEqual([]); + }); + }); + + describe('showPWD / hidePWD', () => { + it('should set dynamictype to text', () => { + component.showPWD(); + expect(component.dynamictype).toBe('text'); + }); + + it('should set dynamictype back to password', () => { + component.showPWD(); + component.hidePWD(); + expect(component.dynamictype).toBe('password'); + }); + }); + + describe('switch', () => { + it('should set passwordSection true and questionsection false', () => { + component.switch(); + expect(component.passwordSection).toBeTrue(); + expect(component.questionsection).toBeFalse(); + }); + }); + + describe('updateQuestions', () => { + it('should add a new question to selectedQuestions at the given position', () => { + component.updateQuestions('q1', 0); + expect(component.selectedQuestions[0]).toBe('q1'); + }); + + it('should clear answer1 when position is 0', () => { + component.answer1 = 'old answer'; + component.updateQuestions('q1', 0); + expect(component.answer1).toBe(''); + }); + + it('should clear answer2 when position is 1', () => { + component.answer2 = 'old answer'; + component.updateQuestions('q2', 1); + expect(component.answer2).toBe(''); + }); + + it('should clear answer3 when position is 2', () => { + component.answer3 = 'old answer'; + component.updateQuestions('q3', 2); + expect(component.answer3).toBe(''); + }); + + it('should alert when same question is selected at a different position', () => { + component.selectedQuestions[0] = 'q1'; + component.updateQuestions('q1', 1); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'This question is already selected. Choose unique question' + ); + }); + + it('should not alert when same question is reselected at the same position', () => { + component.selectedQuestions[0] = 'q1'; + component.updateQuestions('q1', 0); + expect(mockConfirmationService.alert).not.toHaveBeenCalled(); + }); + }); + + describe('filter_function', () => { + it('should remove the item with matching QuestionID', () => { + const array = [ + { QuestionID: 1, Question: 'Q1' }, + { QuestionID: 2, Question: 'Q2' }, + { QuestionID: 3, Question: 'Q3' }, + ]; + const result = component.filter_function(2, array); + expect(result.length).toBe(2); + expect(result.find((q: any) => q.QuestionID === 2)).toBeUndefined(); + }); + + it('should return same array when no match', () => { + const array = [{ QuestionID: 1, Question: 'Q1' }]; + const result = component.filter_function(999, array); + expect(result.length).toBe(1); + }); + }); + + describe('filterArrayOne', () => { + it('should filter Q_array_one and Q_array_two', () => { + component.Q_array_one = [...mockQuestions]; + component.Q_array_two = [...mockQuestions]; + component.filterArrayOne(1); + expect( + component.Q_array_one.find((q: any) => q.QuestionID === 1) + ).toBeUndefined(); + expect( + component.Q_array_two.find((q: any) => q.QuestionID === 1) + ).toBeUndefined(); + }); + }); + + describe('filterArrayTwo', () => { + it('should filter Q_array_two and questions', () => { + component.Q_array_two = [...mockQuestions]; + component.questions = [...mockQuestions]; + component.filterArrayTwo(2); + expect( + component.Q_array_two.find((q: any) => q.QuestionID === 2) + ).toBeUndefined(); + expect( + component.questions.find((q: any) => q.QuestionID === 2) + ).toBeUndefined(); + }); + }); + + describe('filterArrayThree', () => { + it('should filter Q_array_one and questions', () => { + component.Q_array_one = [...mockQuestions]; + component.questions = [...mockQuestions]; + component.filterArrayThree(3); + expect( + component.Q_array_one.find((q: any) => q.QuestionID === 3) + ).toBeUndefined(); + expect( + component.questions.find((q: any) => q.QuestionID === 3) + ).toBeUndefined(); + }); + }); + + describe('setSecurityQuestions', () => { + it('should build dataArray and call switch when 3 unique questions selected', () => { + spyOn(component, 'switch'); + component.selectedQuestions = ['q1', 'q2', 'q3']; + component.question1 = 1; + component.question2 = 2; + component.question3 = 3; + component.answer1 = 'a1'; + component.answer2 = 'a2'; + component.answer3 = 'a3'; + + component.setSecurityQuestions(); + + expect(component.dataArray.length).toBe(3); + expect(component.dataArray[0].questionID).toBe(1); + expect(component.dataArray[0].answers).toBe('a1'); + expect(component.dataArray[0].userID).toBe('456'); + expect(component.dataArray[0].createdBy).toBe('testuser'); + expect(component.switch).toHaveBeenCalled(); + }); + + it('should alert when fewer than 3 questions are selected', () => { + component.selectedQuestions = ['q1', 'q2']; + component.setSecurityQuestions(); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'All 3 questions should be different. Please check your selected questions' + ); + }); + }); + + describe('encrypt', () => { + it('should return an encrypted string', () => { + const result = component.encrypt('Piramal12Piramal', 'testPwd'); + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + }); + + it('should produce different outputs due to random salt/IV', () => { + const r1 = component.encrypt('Piramal12Piramal', 'testPwd'); + const r2 = component.encrypt('Piramal12Piramal', 'testPwd'); + expect(r1).not.toBe(r2); + }); + }); + + describe('keySize getter/setter', () => { + it('should get and set keySize', () => { + expect(component.keySize).toBe(256); + component.keySize = 512; + expect(component.keySize).toBe(512); + }); + }); + + describe('iterationCount getter/setter', () => { + it('should get and set iterationCount', () => { + expect(component.iterationCount).toBe(1989); + component.iterationCount = 3000; + expect(component.iterationCount).toBe(3000); + }); + }); + + describe('updatePassword', () => { + it('should call saveUserSecurityQuestionsAnswer when passwords match', () => { + component.confirmpwd = 'NewPass1!'; + component.dataArray = [{ questionID: 1, answers: 'a', userID: '456' }]; + mockAuthService.saveUserSecurityQuestionsAnswer.and.returnValue( + of({ statusCode: 200, data: { transactionId: 'txn1' } }) + ); + mockAuthService.setNewPassword.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(true)); + + component.updatePassword('NewPass1!'); + + expect( + mockAuthService.saveUserSecurityQuestionsAnswer + ).toHaveBeenCalledWith(component.dataArray); + }); + + it('should alert when passwords do not match', () => { + component.confirmpwd = 'Other'; + component.updatePassword('NewPass1!'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + "Password doesn't match" + ); + }); + + it('should encrypt the password before sending', () => { + component.confirmpwd = 'NewPass1!'; + component.dataArray = []; + mockAuthService.saveUserSecurityQuestionsAnswer.and.returnValue( + of({ statusCode: 200, data: { transactionId: 'txn1' } }) + ); + mockAuthService.setNewPassword.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(true)); + + component.updatePassword('NewPass1!'); + + expect(component.password).toBeTruthy(); + expect(component.password).not.toBe('NewPass1!'); + }); + }); + + describe('handleQuestionSaveSuccess', () => { + it('should call setNewPassword when transactionId exists', () => { + const response = { + statusCode: 200, + data: { transactionId: 'txn1' }, + }; + mockAuthService.setNewPassword.and.returnValue( + of({ statusCode: 200, data: {} }) + ); + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(true)); + + component.handleQuestionSaveSuccess(response, 'encPwd'); + + expect(mockAuthService.setNewPassword).toHaveBeenCalledWith( + 'testuser', + 'encPwd', + 'txn1' + ); + }); + + it('should alert error when response has no valid transactionId', () => { + const response = { + statusCode: 400, + errorMessage: 'Save failed', + data: {}, + }; + component.handleQuestionSaveSuccess(response, 'encPwd'); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Save failed', + 'error' + ); + }); + + it('throws on a null response (else-branch reads response.errorMessage, current behaviour)', () => { + expect(() => + component.handleQuestionSaveSuccess(null, 'encPwd') + ).toThrowError(TypeError); + expect(mockAuthService.setNewPassword).not.toHaveBeenCalled(); + }); + }); + + describe('handleQuestionSaveError', () => { + it('should log the error', () => { + spyOn(console, 'log'); + component.handleQuestionSaveError({ message: 'error' }); + expect(console.log).toHaveBeenCalledWith('question save error', { + message: 'error', + }); + }); + }); + + describe('successCallback', () => { + it('should alert success and call logout', () => { + spyOn(component, 'logout'); + component.successCallback({ statusCode: 200 }); + expect(mockConfirmationService.alert).toHaveBeenCalledWith( + 'Password changed successfully', + 'success' + ); + expect(component.logout).toHaveBeenCalled(); + }); + }); + + describe('errorCallback', () => { + it('should log the response', () => { + spyOn(console, 'log'); + component.errorCallback({ message: 'err' }); + expect(console.log).toHaveBeenCalledWith({ message: 'err' }); + }); + }); + + describe('logout', () => { + it('should call authService.logout and navigate to /login', () => { + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(true)); + spyOn(sessionStorage, 'clear'); + + component.logout(); + + expect(mockAuthService.logout).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']); + }); + + it('should clear sessionStorage on successful navigation', done => { + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(true)); + spyOn(sessionStorage, 'clear'); + + component.logout(); + + setTimeout(() => { + expect(sessionStorage.clear).toHaveBeenCalled(); + done(); + }); + }); + + it('should not clear sessionStorage when navigation returns false', done => { + mockAuthService.logout.and.returnValue(of({})); + mockRouter.navigate.and.returnValue(Promise.resolve(false)); + spyOn(sessionStorage, 'clear'); + + component.logout(); + + setTimeout(() => { + expect(sessionStorage.clear).not.toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('passwordPattern', () => { + it('should match valid passwords', () => { + expect(component.passwordPattern.test('Test1234!')).toBeTrue(); + expect(component.passwordPattern.test('Abcdefg1@')).toBeTrue(); + }); + + it('should reject passwords without uppercase', () => { + expect(component.passwordPattern.test('test1234!')).toBeFalse(); + }); + + it('should reject passwords without numbers', () => { + expect(component.passwordPattern.test('Testtest!')).toBeFalse(); + }); + + it('should reject passwords without special characters', () => { + expect(component.passwordPattern.test('Test12345')).toBeFalse(); + }); + + it('should reject passwords shorter than 8 chars', () => { + expect(component.passwordPattern.test('Te1!')).toBeFalse(); + }); + + it('should reject passwords longer than 12 chars', () => { + expect(component.passwordPattern.test('Test1234567890!')).toBeFalse(); + }); + }); }); diff --git a/src/app/app-modules/tm-logout/tm-logout.component.spec.ts b/src/app/app-modules/tm-logout/tm-logout.component.spec.ts index 7469e1e7..d44e3b3f 100644 --- a/src/app/app-modules/tm-logout/tm-logout.component.spec.ts +++ b/src/app/app-modules/tm-logout/tm-logout.component.spec.ts @@ -20,27 +20,38 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { TmLogoutComponent } from './tm-logout.component'; +import { + commonTestProviders, + COMMON_TEST_IMPORTS, + NO_ERRORS_SCHEMA, +} from 'src/testing/test-utils'; describe('TmLogoutComponent', () => { - let component: TmLogoutComponent; let fixture: ComponentFixture; + let router: Router; + let clearSpy: jasmine.Spy; - beforeEach(async(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [...COMMON_TEST_IMPORTS], declarations: [TmLogoutComponent], + providers: [...commonTestProviders()], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - })); - - beforeEach(() => { + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + clearSpy = spyOn(Storage.prototype, 'clear'); fixture = TestBed.createComponent(TmLogoutComponent); - component = fixture.componentInstance; fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('clears session storage and navigates to login on init', () => { + expect(fixture.componentInstance).toBeTruthy(); + expect(clearSpy).toHaveBeenCalledTimes(1); + expect(router.navigate).toHaveBeenCalledWith(['/login']); }); }); diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 149e74fa..9859361d 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,33 +1,97 @@ -import { TestBed } from '@angular/core/testing'; +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; import { AppComponent } from './app.component'; +import { SpinnerService } from './app-modules/core/services'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; + +@Component({ selector: 'app-spinner', template: '' }) +class MockSpinnerComponent {} describe('AppComponent', () => { - beforeEach(() => + let component: AppComponent; + let fixture: ComponentFixture; + let mockSpinnerService: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + mockSpinnerService = jasmine.createSpyObj('SpinnerService', [ + 'show', + 'hide', + ]); + + const mockTrackingService = jasmine.createSpyObj('AmritTrackingService', [ + 'setUserId', + 'initializeTracker', + 'trackPageView', + ]); + TestBed.configureTestingModule({ imports: [RouterTestingModule], - declarations: [AppComponent], - }) - ); + declarations: [AppComponent, MockSpinnerComponent], + providers: [ + { provide: SpinnerService, useValue: mockSpinnerService }, + { + provide: AmritTrackingService, + useValue: mockTrackingService, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + }); it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); + expect(component).toBeTruthy(); }); - it(`should have as title 'MMU-UI'`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('MMU-UI'); + it('should have isAuthenticated set to false by default', () => { + expect(component.isAuthenticated).toBeFalse(); + }); + + it('should have showRoles set to false by default', () => { + expect(component.showRoles).toBeFalse(); + }); + + it('should allow showRoles to be set as an input', () => { + component.showRoles = true; + expect(component.showRoles).toBeTrue(); + }); + + it('should render router-outlet', () => { + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('router-outlet')).toBeTruthy(); }); - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); + it('should render app-spinner component', () => { fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('.content span')?.textContent).toContain( - 'MMU-UI app is running!' - ); + expect(compiled.querySelector('app-spinner')).toBeTruthy(); }); }); diff --git a/src/testing/test-utils.ts b/src/testing/test-utils.ts new file mode 100644 index 00000000..66cb1a6b --- /dev/null +++ b/src/testing/test-utils.ts @@ -0,0 +1,255 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +/** + * Shared helpers for MMU-UI unit tests. + * + * Typical component setup: + * + * await TestBed.configureTestingModule({ + * imports: [...COMMON_TEST_IMPORTS], + * declarations: [MyComponent], + * providers: [ + * ...commonTestProviders(), + * { provide: DoctorService, useValue: autoSpy(DoctorService) }, + * ], + * schemas: [NO_ERRORS_SCHEMA], + * }).compileComponents(); + */ +import { NO_ERRORS_SCHEMA, Provider, Type } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { DatePipe } from '@angular/common'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogRef, +} from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; + +import { HttpServiceService } from 'src/app/app-modules/core/services/http-service.service'; +import { ConfirmationService } from 'src/app/app-modules/core/services/confirmation.service'; +import { SessionStorageService } from 'Common-UI/src/registrar/services/session-storage.service'; +import { AmritTrackingService } from 'Common-UI/src/tracking'; +import englishJson from 'src/assets/English.json'; + +export { NO_ERRORS_SCHEMA }; + +/** The real English language set, as components receive it from HttpServiceService. */ +export const LANGUAGE_EN: any = (englishJson as any).English; + +/** Default payload returned by auto-spied service methods. */ +export const OK_RESPONSE = { statusCode: 200, data: {} }; + +export type SpyOf = T & { [K in keyof T]: T[K] & jasmine.Spy }; + +/** + * Creates a spy object for every prototype method of `cls`. + * Each spy returns `of(defaultReturn)` so subscribe() calls work out of the box; + * override per test with `spy.method.and.returnValue(...)`. + * Extra instance properties (subjects, flags) can be supplied via `props`. + */ +export function autoSpy( + cls: Type, + props: Partial> = {}, + defaultReturn: any = OK_RESPONSE +): SpyOf { + const obj: any = {}; + let proto = cls.prototype; + while (proto && proto !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(proto)) { + if (name === 'constructor' || name in obj) continue; + const desc = Object.getOwnPropertyDescriptor(proto, name); + if (desc && typeof desc.value === 'function') { + obj[name] = jasmine + .createSpy(`${cls.name}.${name}`) + .and.callFake(() => of(defaultReturn)); + } + } + proto = Object.getPrototypeOf(proto); + } + Object.assign(obj, props); + return obj as SpyOf; +} + +/** HttpServiceService mock that emits the English language set. */ +export function createHttpServiceMock(language: any = LANGUAGE_EN) { + const subject = new BehaviorSubject(language); + const listeners = new Subject(); + return { + language, + appCurrentLanguge: subject, + currentLangugae$: subject.asObservable(), + listen: jasmine + .createSpy('listen') + .and.callFake(() => listeners.asObservable()), + filter: jasmine + .createSpy('filter') + .and.callFake((v: any) => listeners.next(v)), + fetchLanguageSet: jasmine + .createSpy('fetchLanguageSet') + .and.returnValue(of({})), + getCurrentLanguage: jasmine + .createSpy('getCurrentLanguage') + .and.callFake((lang: any) => subject.next(lang)), + }; +} + +/** In-memory SessionStorageService mock. Seed values via `initial`. */ +export function createSessionStorageMock(initial: Record = {}) { + const store = new Map(Object.entries(initial)); + return { + store, + getItem: jasmine + .createSpy('getItem') + .and.callFake((k: string) => (store.has(k) ? store.get(k) : null)), + setItem: jasmine + .createSpy('setItem') + .and.callFake((k: string, v: any) => store.set(k, v)), + removeItem: jasmine + .createSpy('removeItem') + .and.callFake((k: string) => store.delete(k)), + clear: jasmine.createSpy('clear').and.callFake(() => store.clear()), + }; +} + +/** + * ConfirmationService mock. Every dialog resolves immediately with `result` + * (default true). Override per call: `confirmation.confirm.and.returnValue(of(false))`. + */ +export function createConfirmationMock(result: any = true) { + const methods = [ + 'confirm', + 'confirmHealthId', + 'alert', + 'remarks', + 'editRemarks', + 'notify', + 'choice', + 'startTimer', + 'choiceSelect', + 'alertFetsenseMessage', + 'confirmCalibration', + 'confirmCBAC', + 'confirmCareContext', + ]; + const obj: any = {}; + methods.forEach( + m => + (obj[m] = jasmine + .createSpy(`ConfirmationService.${m}`) + .and.returnValue(of(result))) + ); + return obj; +} + +/** MatDialogRef mock whose afterClosed() emits `result`. */ +export function createDialogRefMock(result: any = undefined) { + return { + close: jasmine.createSpy('close'), + afterClosed: jasmine.createSpy('afterClosed').and.returnValue(of(result)), + afterOpened: jasmine + .createSpy('afterOpened') + .and.returnValue(of(undefined)), + beforeClosed: jasmine.createSpy('beforeClosed').and.returnValue(of(result)), + backdropClick: jasmine + .createSpy('backdropClick') + .and.returnValue(of(undefined)), + keydownEvents: jasmine.createSpy('keydownEvents').and.returnValue(of()), + updateSize: jasmine.createSpy('updateSize'), + updatePosition: jasmine.createSpy('updatePosition'), + disableClose: false, + componentInstance: {} as any, + }; +} + +/** MatDialog mock; open() returns a dialog ref whose afterClosed() emits `result`. */ +export function createMatDialogMock(result: any = undefined) { + return { + open: jasmine + .createSpy('MatDialog.open') + .and.callFake(() => createDialogRefMock(result)), + closeAll: jasmine.createSpy('closeAll'), + openDialogs: [], + afterAllClosed: of(undefined), + }; +} + +export function createTrackingMock() { + return autoSpy(AmritTrackingService, {}, undefined) as any; +} + +/** Modules almost every component spec needs. */ +export const COMMON_TEST_IMPORTS = [ + HttpClientTestingModule, + RouterTestingModule, + NoopAnimationsModule, + FormsModule, + ReactiveFormsModule, +]; + +/** + * Providers for the services injected by most components. + * Pass overrides to seed session storage or change dialog results. + */ +export function commonTestProviders( + opts: { + session?: Record; + language?: any; + dialogResult?: any; + dialogData?: any; + } = {} +): Provider[] { + return [ + DatePipe, + { + provide: HttpServiceService, + useValue: createHttpServiceMock(opts.language), + }, + { + provide: SessionStorageService, + useValue: createSessionStorageMock(opts.session), + }, + { provide: ConfirmationService, useValue: createConfirmationMock() }, + { provide: AmritTrackingService, useValue: createTrackingMock() }, + { provide: MatDialog, useValue: createMatDialogMock(opts.dialogResult) }, + { provide: MatDialogRef, useValue: createDialogRefMock(opts.dialogResult) }, + { provide: MAT_DIALOG_DATA, useValue: opts.dialogData ?? {} }, + { + provide: MatSnackBar, + useValue: { open: jasmine.createSpy('snackbar.open') }, + }, + ]; +} + +/** Convenience: typed access to a provider mock from TestBed. */ +export function asSpy(value: unknown): SpyOf { + return value as SpyOf; +} + +/** Observable that errors — for exercising error callbacks. */ +export function throwingObs(err: any = { status: 500, error: 'boom' }) { + return new Observable(s => s.error(err)); +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json index be7e9da7..3080427d 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,12 +3,15 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, "types": [ "jasmine" ] }, "include": [ "src/**/*.spec.ts", + "src/testing/**/*.ts", "src/**/*.d.ts" ] }