blob: 21418ab2706ca4f70c312a321516e20a506a0374 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createSpyObj, SpyObj } from '../testing/mocks';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HeaderComponent } from './header.component';
import { AuthService } from '../core/services/auth.service';
import { NavigationConfigService } from '../core/services/navigation-config.service';
import { Router } from '@angular/router';
import { ViewportService } from '../core/services/viewport.service';
import { EMPTY, of } from 'rxjs';
import { WritableSignal, signal } from '@angular/core';
import { provideTranslateTesting } from '../testing/i18n-testing';
import { BusinessDateManagementService } from '../api';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let authServiceSpy: SpyObj<AuthService>;
let navigationConfigSpy: SpyObj<NavigationConfigService>;
let routerSpy: SpyObj<Router>;
let isMobile: WritableSignal<boolean>;
beforeEach(async () => {
authServiceSpy = Object.assign(createSpyObj<AuthService>(['logout']), {
username: signal('mifos'),
officeName: signal('Head Office'),
});
navigationConfigSpy = createSpyObj(['searchRoutes']);
navigationConfigSpy.searchRoutes.mockReturnValue([]);
// `routerState` and `events` are properties, not methods, so createSpyObj needs them in the
// property bag. The header reads the route tree for the phone header's page title, the same
// way BreadcrumbComponent does; without them it throws in ngOnInit.
routerSpy = Object.assign(createSpyObj<Router>(['navigate', 'navigateByUrl']), {
routerState: { snapshot: { root: { url: [], routeConfig: null, firstChild: null } } },
events: EMPTY,
});
// Pinned, not inherited from the harness. The header renders a different set of controls
// either side of the breakpoint, and Karma runs specs in an iframe narrower than the
// browser window — so the real ViewportService reports "mobile" under ChromeHeadless and
// "desktop" under a headed run, and these assertions passed or failed on which browser CI
// happened to use.
isMobile = signal(false);
await TestBed.configureTestingModule({
imports: [HeaderComponent],
providers: [
...provideTranslateTesting(),
{ provide: AuthService, useValue: authServiceSpy },
{ provide: NavigationConfigService, useValue: navigationConfigSpy },
{ provide: BusinessDateManagementService, useValue: { getBusinessdate: () => of([]) } },
{ provide: Router, useValue: routerSpy },
{ provide: ViewportService, useValue: { isMobile } },
],
}).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display username and office', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.username')?.textContent).toContain('mifos');
expect(compiled.querySelector('.office')?.textContent).toContain('Head Office');
});
it('moves the actions into an overflow menu on a narrow viewport', () => {
// The counterpart to the case above: below the breakpoint those same controls are not in
// the bar at all, and reaching them is what the overflow trigger is for.
isMobile.set(true);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.username')).toBeNull();
expect(compiled.querySelector('#header-overflow')).not.toBeNull();
expect(compiled.querySelector('.page-title')).not.toBeNull();
});
it('should call logout and navigate', () => {
component.logout();
expect(authServiceSpy.logout).toHaveBeenCalled();
expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']);
});
it('should switch language', () => {
const translate = (component as unknown as { translate: { use(language: string): unknown } })
.translate;
const useSpy = vi.spyOn(translate, 'use');
component.switchLanguage('hi');
expect(useSpy).toHaveBeenCalledWith('hi');
});
it('navigates to a page result via navigateByUrl', () => {
component.onResultSelected({
kind: 'nav',
nav: { route: '/organization/offices', label: 'Offices' },
});
expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/organization/offices');
});
it('navigates to an entity result by type', () => {
component.onResultSelected({
kind: 'entity',
entity: { entityType: 'LOAN', entityId: 42 } as never,
});
expect(routerSpy.navigate).toHaveBeenCalledWith(['/loans/view', 42]);
});
it('prevents the mousedown default on a result item, so the searchbar never blurs to start the 150ms hide race', () => {
// Drives `showResults` through the public entry point rather than reaching into the
// (protected) signal directly; `searchResults` is set directly since the debounced
// pipeline behind `onSearchInput` never resolves within a synchronous test.
component.onSearchInput({ detail: { value: 'Offices' } } as unknown as Event);
component.searchResults.set([
{ kind: 'nav', nav: { route: '/organization/offices', label: 'Offices' } },
]);
fixture.detectChanges();
const item = fixture.debugElement.query(By.css('ion-item'));
const event = createSpyObj(['preventDefault']);
item.triggerEventHandler('mousedown', event);
expect(event.preventDefault).toHaveBeenCalled();
});
});