/*! * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. * * Alfresco Example Content Application * * This file is part of the Alfresco Example Content Application. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * The Alfresco Example Content Application is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The Alfresco Example Content Application 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * from Hyland Software. If not, see . */ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AgentsButtonComponent } from './agents-button.component'; import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services'; import { Subject } from 'rxjs'; import { By } from '@angular/platform-browser'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { getAppSelection, SearchAiActionTypes, ToggleAISearchInput } from '@alfresco/aca-shared/store'; import { AvatarComponent, NotificationService } from '@alfresco/adf-core'; import { SelectionState } from '@alfresco/adf-extensions'; import { MatMenu, MatMenuPanel, MatMenuTrigger } from '@angular/material/menu'; import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectionListHarness } from '@angular/material/list/testing'; import { MatMenuHarness } from '@angular/material/menu/testing'; import { MatSelectionList } from '@angular/material/list'; import { MatSnackBarRef } from '@angular/material/snack-bar'; import { ChangeDetectorRef } from '@angular/core'; import { Agent, KnowledgeRetrievalConfigEntry } from '@alfresco/js-api'; describe('AgentsButtonComponent', () => { let component: AgentsButtonComponent; let fixture: ComponentFixture; let agents$: Subject; let agentsMock: Agent[]; let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>; let selectionState: SelectionState; let store: MockStore; let config$: Subject; const knowledgeRetrievalUrl = 'some url'; const getMenu = (): MatMenu => fixture.debugElement.query(By.directive(MatMenu)).componentInstance; const getAgentsButton = (): HTMLButtonElement => fixture.debugElement.query(By.css('.aca-agents-menu-button'))?.nativeElement; const runButtonActions = (eventName: string): void => { let event: Event; let notificationService: NotificationService; let message: string; beforeEach(() => { config$.next({ entry: { knowledgeRetrievalUrl } }); config$.complete(); event = eventName === 'mouseup' ? new MouseEvent(eventName) : new KeyboardEvent(eventName, { key: 'Enter' }); agents$.next(agentsMock); agents$.complete(); spyOn(window, 'open'); notificationService = TestBed.inject(NotificationService); spyOn(notificationService, 'showError'); message = 'Some message'; component.avatarsMocked = false; }); const getMenuTrigger = (): MatMenuPanel => fixture.debugElement.query(By.directive(MatMenuTrigger)).injector.get(MatMenuTrigger).menu; const testButtonActions = (): void => { it('should not display notification if checkSearchAvailability from SearchAiService returns empty message', () => { message = ''; checkSearchAvailabilitySpy.and.returnValue(message); getAgentsButton().dispatchEvent(event); expect(notificationService.showError).not.toHaveBeenCalled(); }); it('should disable menu triggering if checkSearchAvailability from SearchAiService returns message', () => { checkSearchAvailabilitySpy.and.returnValue('Some message'); getAgentsButton().dispatchEvent(event); fixture.detectChanges(); expect(getMenuTrigger()).toBeNull(); }); }; describe('with selected nodes', () => { beforeEach(() => { selectionState.isEmpty = false; }); it('should display notification if checkSearchAvailability from SearchAiService returns message', () => { checkSearchAvailabilitySpy.and.returnValue(message); getAgentsButton().dispatchEvent(event); expect(notificationService.showError).toHaveBeenCalledWith(message); }); testButtonActions(); it('should enable menu triggering if checkSearchAvailability from SearchAiService returns empty message', () => { checkSearchAvailabilitySpy.and.returnValue(''); getAgentsButton().dispatchEvent(event); fixture.detectChanges(); const menuTrigger = getMenuTrigger(); expect(menuTrigger).toBeTruthy(); expect(menuTrigger).toBe(getMenu()); }); it('should call checkSearchAvailability from SearchAiService with correct parameter', () => { getAgentsButton().dispatchEvent(event); expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(selectionState); }); it('should not open new tab for url loaded from config', () => { getAgentsButton().dispatchEvent(event); expect(window.open).not.toHaveBeenCalled(); }); }); describe('without selected nodes', () => { it('should not display notification if checkSearchAvailability from SearchAiService returns message', () => { checkSearchAvailabilitySpy.and.returnValue(message); getAgentsButton().dispatchEvent(event); expect(notificationService.showError).not.toHaveBeenCalled(); }); testButtonActions(); it('should disable menu triggering if checkSearchAvailability from SearchAiService returns empty message', () => { checkSearchAvailabilitySpy.and.returnValue(''); getAgentsButton().dispatchEvent(event); fixture.detectChanges(); expect(getMenuTrigger()).toBeNull(); }); it('should not call checkSearchAvailability from SearchAiService', () => { getAgentsButton().dispatchEvent(event); expect(checkSearchAvailabilitySpy).not.toHaveBeenCalled(); }); it('should open new tab for url loaded from config', () => { getAgentsButton().dispatchEvent(event); expect(window.open).toHaveBeenCalledWith(knowledgeRetrievalUrl); }); }); }; beforeEach(() => { TestBed.configureTestingModule({ imports: [AgentsButtonComponent, ContentTestingModule], providers: [provideMockStore({})] }); fixture = TestBed.createComponent(AgentsButtonComponent); component = fixture.componentInstance; store = TestBed.inject(MockStore); agents$ = new Subject(); spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$); agentsMock = [ { id: '1', name: 'HR Agent', description: 'Test 1', avatarUrl: undefined }, { id: '2', name: 'Policy Agent', description: 'Test 2', avatarUrl: undefined } ]; const searchAiService = TestBed.inject(SearchAiService); checkSearchAvailabilitySpy = spyOn(searchAiService, 'checkSearchAvailability'); config$ = new Subject(); spyOn(searchAiService, 'getConfig').and.returnValue(config$); selectionState = { nodes: [], isEmpty: true, count: 0, libraries: [] }; store.overrideSelector(getAppSelection, selectionState); fixture.detectChanges(); }); afterEach(() => { store.resetSelectors(); }); describe('Button', () => { let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef>; beforeEach(() => { const notificationService = TestBed.inject(NotificationService); notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough(); }); describe('loaded config', () => { beforeEach(() => { component.avatarsMocked = false; config$.next({ entry: { knowledgeRetrievalUrl } }); config$.complete(); }); it('should be rendered if any agentsMock are loaded', () => { agents$.next(agentsMock); agents$.complete(); fixture.detectChanges(); expect(getAgentsButton()).toBeTruthy(); }); it('should get agentsMock on component init', () => { agents$.next(agentsMock); agents$.complete(); component.ngOnInit(); expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' }); expect(component.agents).toEqual(agentsMock); expect(notificationServiceSpy).not.toHaveBeenCalled(); }); it('should run detectChanges when getting the agentsMock', () => { const changeDetectorRef2 = fixture.debugElement.injector.get(ChangeDetectorRef); const detectChangesSpy = spyOn(changeDetectorRef2.constructor.prototype, 'detectChanges'); component.ngOnInit(); agents$.next(agentsMock); expect(detectChangesSpy).toHaveBeenCalled(); }); it('should show notification error on getAgents error', () => { agents$.error('error'); component.ngOnInit(); expect(component.agents).toEqual([]); expect(component.initialsByAgentId).toEqual({}); expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.AGENTS_FETCHING'); }); it('should not be rendered if none agent is loaded', () => { agentsMock = []; agents$.next(agentsMock); agents$.complete(); fixture.detectChanges(); expect(getAgentsButton()).toBeFalsy(); }); it('should have correct label', () => { agents$.next(agentsMock); agents$.complete(); fixture.detectChanges(); expect(getAgentsButton().textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.AGENTS_BUTTON.LABEL'); }); it('should contain stars icon', () => { agents$.next(agentsMock); agents$.complete(); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.aca-agents-menu-button adf-icon')).componentInstance.value).toBe('adf:colored-stars-ai'); }); }); describe('loaded config with error', () => { beforeEach(() => { config$.error('error'); config$.complete(); }); it('should not be rendered', () => { agents$.next(agentsMock); agents$.complete(); fixture.detectChanges(); expect(getAgentsButton()).toBeFalsy(); }); it('should show notification error', () => { agents$.next(agentsMock); agents$.complete(); component.ngOnInit(); expect(component.hxInsightUrl).toBeUndefined(); expect(notificationServiceSpy).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.ERRORS.HX_INSIGHT_URL_FETCHING'); }); }); }); const buttonKeyboardActions = (eventName: string): void => { describe(`Button action - ${eventName} event`, () => { runButtonActions(eventName); }); }; ['mouseup', 'keydown'].forEach((eventName) => { buttonKeyboardActions(eventName); }); describe('Agents menu', () => { let loader: HarnessLoader; const prepareData = (agents: Agent[]): void => { component.avatarsMocked = false; config$.next({ entry: { knowledgeRetrievalUrl } }); config$.complete(); loader = TestbedHarnessEnvironment.loader(fixture); agents$.next(agents); selectionState.isEmpty = false; checkSearchAvailabilitySpy.and.returnValue(''); const button = getAgentsButton(); button.dispatchEvent(new MouseEvent('mouseup')); fixture.detectChanges(); button.click(); fixture.detectChanges(); }; const getAvatar = (agentId: string): AvatarComponent => fixture.debugElement.query(By.css(`[data-automation-id=aca-agents-button-agent-${agentId}]`)).query(By.directive(AvatarComponent)) .componentInstance; describe('Agents position', () => { it('should have assigned before to xPosition', () => { prepareData(agentsMock); agents$.complete(); expect(getMenu().xPosition).toBe('before'); }); }); describe('Agents multi words name', () => { beforeEach(() => { prepareData(agentsMock); agents$.complete(); }); const getAgentsListHarness = async (): Promise => (await loader.getHarness(MatMenuHarness)).getHarness(MatSelectionListHarness); const selectAgent = async (): Promise => (await getAgentsListHarness()).selectItems({ fullText: 'PA Policy Agent' }); const getAgentsList = (): MatSelectionList => fixture.debugElement.query(By.directive(MatSelectionList)).componentInstance; it('should deselect selected agent after selecting other', async () => { component.data = { trigger: SearchAiActionTypes.ToggleAiSearchInput }; const selectionList = getAgentsList(); spyOn(selectionList, 'deselectAll'); await selectAgent(); expect(selectionList.deselectAll).toHaveBeenCalled(); }); it('should dispatch on store selected agent', async () => { component.data = { trigger: SearchAiActionTypes.ToggleAiSearchInput }; spyOn(store, 'dispatch'); await selectAgent(); expect(store.dispatch).toHaveBeenCalledWith({ type: SearchAiActionTypes.ToggleAiSearchInput, agentId: '2' }); }); it('should disallow selecting multiple agentsMock', () => { expect(getAgentsList().multiple).toBeFalse(); }); it('should have hidden single selection indicator', () => { expect(getAgentsList().hideSingleSelectionIndicator).toBeTrue(); }); it('should display option for each agent', async () => { const agents = await (await getAgentsListHarness()).getItems(); expect(agents.length).toBe(2); expect(await agents[0].getFullText()).toBe('HA HR Agent'); expect(await agents[1].getFullText()).toBe('PA Policy Agent'); }); it('should display avatar for each agent', () => { expect(getAvatar('1')).toBeTruthy(); expect(getAvatar('2')).toBeTruthy(); }); it('should assign correct initials to each avatar for each agent with double section name', () => { expect(getAvatar('1').initials).toBe('HA'); expect(getAvatar('2').initials).toBe('PA'); }); }); describe('Agents multi words name', () => { it('should assign correct initials to each avatar for each agent with single section name', () => { agentsMock = [ { id: '1', name: 'HR Agent', description: 'Test 1', avatarUrl: undefined }, { id: '2', name: 'Policy Agent', description: 'Test 2', avatarUrl: undefined } ]; agentsMock[0].name = 'Adam'; agentsMock[1].name = 'Bob'; prepareData(agentsMock); expect(getAvatar('1').initials).toBe('A'); expect(getAvatar('2').initials).toBe('B'); }); }); }); });