jacekpluta edcc6f8047
[ACS-8201] Knowledge Retrieval - getting AI response for one or more selected files (#4127)
* [ACS-8202] basic flow getting ai response for one or more selected files (#3936)

* ACS-8202 Added animated icon

* ACS-8202 Added search ai input

* ACS-8202 Added AI search results page

* ACS-8202 Allow to run knowledge retrieval on files inside library, shared, favourites and recent files

* ACS-8202 Hide icon when selected more than 100 files or non text files

* ACS-8202 Display notification when too many files are selected

* ACS-8202 Added agents dropdown

* ACS-8202 Styles for AI response

* ACS-8202 Applied design changes

* ACS-8202 Added query card to Knowledge retrieval page results

* ACS-8202 Fixed search collapsing when opened results page

* ACS-8202 Changed placeholder in input for results page, wrapping text and scrolling for results page

* ACS-8202 Display snackbar with messages when conditions are not met

* ACS-8202 Disallow run knowledge retrieval for libraries, leave input when click on x button

* ACS-8202 Renaming files

* ACS-8202 Trigger ai input by selecting agent instead of clicking on button

* ACS-8202 Reverted triggering showing input by selecting option from select

* ACS-8202 Display dropdown with agents by clicking on button

* ACS-8202 Structural changes - services and agents button component

* ACS-8202 Removed part for examples from search page

* ACS-8202 Simplified html for search page

* ACS-8202 Refactored html and styles for search page, translations for search page

* ACS-8202 More html and styles refactoring

* ACS-8202 Formatting html

* ACS-8202 Removed references to angular material classes

* ACS-8202 Added data automation id attributes

* ACS-8202 Load agents from backend, formatting html for agents button component and adding data automation ids to that component

* ACS-8202 Correction after rebase

* ACS-8202 Set agent for input based on selected agent from dropdown for agents button

* ACS-8202 Hide agent button for libraries pages and use translations for warnings when clicked on agents button

* ACS-8202 Pass agent id to search results page

* ACS-8202 Used form control instead of ngmodel for search query

* ACS-8202 Moved search ai service and search ai input state to ADF

* ACS-8202 Results page ts clean up

* ACS-8202 Used ask and getAnswer functions from search ai service

* ACS-8202 Cleaning of search ai navigation service

* ACS-8202 Small clean ups

* ACS-8202 Renamed sources to references

* ACS-8202 Fixed asking next question from results page

* ACS-8202 Added possibility to use knowledge retrieval from search results page

* ACS-8202 Fixed issue with selecting the same agent after previously closing input on search results page

* ACS-8202 Disallowed using knowledge retrieval on trash page

* ACS-8202 Hide toggling knowledge retrieval for tasks and processes, fixed displaying ask button for favorites page

* ACS-8202 Removed redundant image and function

* ACS-8202 Renamed breadcrumbTemplate to header

* ACS-8202 Removed redundant code, added some comments, made some fields as private

* ACS-8202 Display error message on search page

* ACS-8202 Accessibility changes

* ACS-8202 Small correction

* ACS-8202 Addressed comments

* ACS-8202 Displayed correct initials

* ACS-8202 Removed redundant imports

* ACS-8202 Change css value

* ACS-8202 Removed icon animation

* ACS-8202 Removed icon animation

* ACS-8201 Small correction after rebasing with Angular 15

* [ACS-8398] unit tests (#3973)

* ACS-8398 Unit tests for agents button and part for agents menu

* ACS-8398 Unit tests for search ai input component

* [ACS-8210] Agent basic details popup (#3942)

* [ACS-8210] Agent basic details popup

* [ACS-8210] Agent basic details popup - review fixes

---------

Co-authored-by: Aleksander Sklorz <aleksander.sklorz@hyland.com>

* [ACS-8382] Blurring the AI answer section before getting response from backend (#3948)

* [ACS-8398] Unit tests part 2 (#3989)

* ACS-8398 Unit tests for search ai input container

* ACS-8398 Unit tests for search ai navigation service and rest tests for search ai input container component

* ACS-8398 Added missing type

* [ACS-8484] Add feature flag to knowledge retrieval (#4003)

* [ACS-8562] "Ask Agent" button name is changed to "Ask Discovery"

* [ACS-8573] Allow user to ask question without file selection

* [ACS-8312] Display warning about losing response (#4012)

* ACS-8201 Fixed issues after rebase

* [ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page (#4030)

[ACS-8588] Navigation is triggered twice when leaving Knowledge Retrieval page

* [ACS-8399] Integrate all changes with backend (#4076)

* [ACS-8399] Integrate all changes with backend

* [ACS-8399] Integrate all changes with backend - review fixes

* Answers endpoint fix

* Answers endpoint fix (#4107)

* [ACS-8664] generic question redirection to hx insight page (#4102)

* ACS-8664 Open page in new tab

* ACS-8664 Loading HX insight url

* ACS-8664 Unit tests

* ACS-8664 Fix after rebasing

* ACS-8664 Fixed unit tests

* ACS-8664 Added type

* ACS-8664 Removed duplicated lines

* ACS-8664 Removed duplicated lines

* ACS-8664 Addressed comments

* [ACS-8695] Getting Agent avatar (#4110)

* [ACS-8695] Getting Agent avatar

* [ACS-8695] Getting Agent avatar - fixes

* [ACS-8695] Getting Agent avatar - fixes 2

* Adding mocked agent avatars (#4117)

* [ACS-8201] review fixes

* [ACS-8201] review fixes

* [E2E] excluded failing tests to fix later pt.1

* [ACS-8767] allow to open referenced files (#4129)

* ACS-8767 Opening referenced files

* ACS-8767 Reverted one line

* ACS-8767 Removed unwanted code

* ACS-8767

* ACS-8767 Unit tests for allowing clicking on references

* ACS-8767 Unit tests

* ACS-8767 Moved duplicated code to function

* ACS-8767 Resolved sonar issue

* ACS-8767 Resolved sonar issue

* [ACS-8201] knowledge retrieval feature flag - false

* [E2E] excluded failing tests to fix later pt.2

* ACS-8201 Fixed tests

---------

Co-authored-by: AleksanderSklorz <115619721+AleksanderSklorz@users.noreply.github.com>
Co-authored-by: Aleksander Sklorz <Aleksander.Sklorz@hyland.com>
Co-authored-by: datguychen <adam.swiderski@hyland.com>
2024-09-20 14:47:41 +02:00

432 lines
16 KiB
TypeScript

/*!
* 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 <http://www.gnu.org/licenses/>.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchAiInputComponent } from './search-ai-input.component';
import { MatSelect, MatSelectModule } from '@angular/material/select';
import { By } from '@angular/platform-browser';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { AgentService, ContentTestingModule, SearchAiService } from '@alfresco/adf-content-services';
import { getAppSelection, SearchByTermAiAction } from '@alfresco/aca-shared/store';
import { of, Subject } from 'rxjs';
import { Agent, NodeEntry } from '@alfresco/js-api';
import { FormControlDirective } from '@angular/forms';
import { DebugElement } from '@angular/core';
import { AvatarComponent, IconComponent, NotificationService, UnsavedChangesDialogComponent, UserPreferencesService } from '@alfresco/adf-core';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';
import { MatOptionHarness } from '@angular/material/core/testing';
import { MatInput } from '@angular/material/input';
import { MatButton } from '@angular/material/button';
import { MatInputHarness } from '@angular/material/input/testing';
import { SelectionState } from '@alfresco/adf-extensions';
import { MatSnackBarRef } from '@angular/material/snack-bar';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { ModalAiService } from '../../../../services/modal-ai.service';
const agentList: Agent[] = [
{
id: '1',
name: 'HR Agent',
description: 'Test 1',
avatarUrl: undefined
},
{
id: '2',
name: 'Policy Agent',
description: 'Test 2',
avatarUrl: undefined
}
];
describe('SearchAiInputComponent', () => {
let component: SearchAiInputComponent;
let fixture: ComponentFixture<SearchAiInputComponent>;
let loader: HarnessLoader;
let selectionState: SelectionState;
let store: MockStore;
let agents$: Subject<Agent[]>;
let dialog: MatDialog;
const prepareBeforeTest = (): void => {
selectionState = {
nodes: [],
isEmpty: true,
count: 0,
libraries: []
};
store.overrideSelector(getAppSelection, selectionState);
component.agentId = '2';
component.avatarsMocked = false;
component.ngOnInit();
fixture.detectChanges();
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SearchAiInputComponent, ContentTestingModule, MatSelectModule],
providers: [
provideMockStore(),
{
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParams: { query: 'some query' }
}
}
}
]
});
fixture = TestBed.createComponent(SearchAiInputComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
loader = TestbedHarnessEnvironment.loader(fixture);
agents$ = new Subject<Agent[]>();
dialog = TestBed.inject(MatDialog);
spyOn(TestBed.inject(AgentService), 'getAgents').and.returnValue(agents$);
prepareBeforeTest();
});
afterEach(() => {
store.resetSelectors();
});
describe('Agent select box', () => {
let selectElement: DebugElement;
let notificationServiceSpy: jasmine.Spy<(message: string) => MatSnackBarRef<any>>;
beforeEach(() => {
selectElement = fixture.debugElement.query(By.directive(MatSelect));
const notificationService = TestBed.inject(NotificationService);
notificationServiceSpy = spyOn(notificationService, 'showError').and.callThrough();
});
it('should have assigned formControl', () => {
expect(selectElement.injector.get(FormControlDirective).form).toBe(component.agentControl);
});
it('should have hidden single selection indicator', () => {
expect(selectElement.componentInstance.hideSingleSelectionIndicator).toBeTrue();
});
it('should get agents on init', () => {
agents$.next(agentList);
component.ngOnInit();
expect(component.agents).toEqual(agentList);
expect(component.initialsByAgentId).toEqual({ 1: 'HA', 2: 'PA' });
expect(notificationServiceSpy).not.toHaveBeenCalled();
});
it('should show notification 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 have selected correct agent', async () => {
agents$.next(agentList);
expect(await (await loader.getHarness(MatSelectHarness)).getValueText()).toBe('PAPolicy Agent');
const avatar = selectElement.query(By.directive(AvatarComponent))?.componentInstance;
expect(avatar.initials).toBe('PA');
expect(avatar.size).toBe('26px');
});
describe('Agents options', () => {
let options: MatOptionHarness[];
const getAvatarForAgent = (agentId: string): AvatarComponent =>
fixture.debugElement.query(By.css(`[data-automation-id=aca-search-ai-input-agent-${agentId}]`)).query(By.directive(AvatarComponent))
.componentInstance;
beforeEach(async () => {
agents$.next(agentList);
const selectHarness = await loader.getHarness(MatSelectHarness);
await selectHarness.open();
options = await selectHarness.getOptions();
});
it('should have correct number of agents', () => {
expect(options.length).toBe(2);
});
it('should have correct agent names', async () => {
expect(await options[0].getText()).toBe('HAHR Agent');
expect(await options[1].getText()).toBe('PAPolicy Agent');
});
it('should display avatar for each agent', () => {
expect(getAvatarForAgent('1')).toBeTruthy();
expect(getAvatarForAgent('2')).toBeTruthy();
});
it('should have correct initials for avatars for each of agent', () => {
expect(getAvatarForAgent('1').initials).toBe('HA');
expect(getAvatarForAgent('2').initials).toBe('PA');
});
it('should assign correct initials to each avatar for each agent with single section name', () => {
const newAgentList = [
{ ...agentList[0], name: 'Adam' },
{ ...agentList[1], name: 'Bob' }
];
agents$.next(newAgentList);
fixture.detectChanges();
expect(getAvatarForAgent('1').initials).toBe('A');
expect(getAvatarForAgent('2').initials).toBe('B');
});
});
});
describe('Query input', () => {
let queryInput: DebugElement;
beforeEach(() => {
queryInput = fixture.debugElement.query(By.directive(MatInput));
agents$.next(agentList);
});
it('should have assigned formControl', () => {
fixture.detectChanges();
expect(queryInput.injector.get(FormControlDirective).form).toBe(component.queryControl);
});
it('should have assigned correct placeholder', () => {
component.placeholder = 'Please ask your question with as much detail as possible...';
expect(queryInput.componentInstance.placeholder).toBe(component.placeholder);
});
testSubmitting(false);
});
describe('Submit button', () => {
let submitButton: DebugElement;
let queryInput: MatInputHarness;
beforeEach(async () => {
submitButton = fixture.debugElement.query(By.directive(MatButton));
queryInput = await loader.getHarness(MatInputHarness);
agents$.next(agentList);
});
it('should be disabled by default', () => {
expect(submitButton.nativeElement.disabled).toBeTrue();
});
it('should be enabled if query input is filled', async () => {
await queryInput.setValue('Some question');
expect(submitButton.nativeElement.disabled).toBeFalse();
});
it('should be disabled if query input was filled but after that it was emptied', async () => {
await queryInput.setValue('Some question');
await queryInput.setValue('');
expect(submitButton.nativeElement.disabled).toBeTrue();
});
it('should contain stars icon', () => {
expect(submitButton.query(By.directive(IconComponent)).componentInstance.value).toBe('adf:three_magic_stars_ai');
});
it('should have correct label', () => {
expect(submitButton.nativeElement.textContent.trim()).toBe('KNOWLEDGE_RETRIEVAL.SEARCH.SEARCH_INPUT.ASK_BUTTON_LABEL');
});
testSubmitting();
});
function testSubmitting(useButton = true) {
describe('Submitting', () => {
let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
let notificationService: NotificationService;
let userPreferencesService: UserPreferencesService;
let submitButton: DebugElement;
let queryInput: MatInputHarness;
let submittingTrigger: () => void;
const query = 'some query';
let dialogOpenSpy: jasmine.Spy<<T, R>(component: typeof UnsavedChangesDialogComponent, config?: MatDialogConfig) => MatDialogRef<T, R>>;
let modalAiService: ModalAiService;
beforeEach(async () => {
prepareBeforeTest();
modalAiService = TestBed.inject(ModalAiService);
checkSearchAvailabilitySpy = spyOn(TestBed.inject(SearchAiService), 'checkSearchAvailability');
notificationService = TestBed.inject(NotificationService);
userPreferencesService = TestBed.inject(UserPreferencesService);
spyOn(userPreferencesService, 'set');
spyOn(notificationService, 'showError');
queryInput = await loader.getHarness(MatInputHarness);
submitButton = fixture.debugElement.query(By.directive(MatButton));
await queryInput.setValue(query);
const inputElement = fixture.debugElement.query(By.directive(MatInput)).nativeElement;
dialogOpenSpy = spyOn(dialog, 'open').and.returnValue({
afterClosed: () => of(true)
} as MatDialogRef<MatDialog>);
submittingTrigger = useButton
? () => submitButton.nativeElement.click()
: () =>
inputElement.dispatchEvent(
new KeyboardEvent('keyup', {
key: 'Enter'
})
);
});
it('should call showError on NotificationService if checkSearchAvailability from SearchAiService returns message', () => {
const message = 'Some message';
checkSearchAvailabilitySpy.and.returnValue(message);
submittingTrigger();
expect(notificationService.showError).toHaveBeenCalledWith(message);
});
it('should not call showError on NotificationService if checkSearchAvailability from SearchAiService returns empty message', () => {
checkSearchAvailabilitySpy.and.returnValue('');
submittingTrigger();
expect(notificationService.showError).not.toHaveBeenCalled();
});
it('should call checkSearchAvailability on SearchAiService with parameter based on value returned by store', () => {
submittingTrigger();
expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(selectionState);
});
it('should call checkSearchAvailability on SearchAiService with parameter based on value returned by UserPreferencesService', () => {
component.useStoredNodes = true;
const newSelectionState: SelectionState = {
...selectionState,
file: {
entry: {
id: 'some-id'
}
} as NodeEntry
};
spyOn(userPreferencesService, 'get').and.returnValue(JSON.stringify(newSelectionState));
component.ngOnInit();
submittingTrigger();
expect(checkSearchAvailabilitySpy).toHaveBeenCalledWith(newSelectionState);
expect(userPreferencesService.get).toHaveBeenCalledWith('knowledgeRetrievalNodes');
});
it('should call set on UserPreferencesService with parameter based on value returned by store', () => {
submittingTrigger();
expect(userPreferencesService.set).toHaveBeenCalledWith('knowledgeRetrievalNodes', JSON.stringify(selectionState));
});
it('should call set on UserPreferencesService with parameter based on value returned by UserPreferencesService', () => {
component.useStoredNodes = true;
const newSelectionState: SelectionState = {
...selectionState,
file: {
entry: {
id: 'some-id'
}
} as NodeEntry
};
spyOn(userPreferencesService, 'get').and.returnValue(JSON.stringify(newSelectionState));
component.ngOnInit();
submittingTrigger();
expect(userPreferencesService.get).toHaveBeenCalledWith('knowledgeRetrievalNodes');
expect(userPreferencesService.set).toHaveBeenCalledWith('knowledgeRetrievalNodes', JSON.stringify(newSelectionState));
});
it('should call dispatch on store with correct parameter', () => {
spyOn(store, 'dispatch');
submittingTrigger();
expect(store.dispatch).toHaveBeenCalledOnceWith(
new SearchByTermAiAction({
searchTerm: query,
agentId: component.agentId
})
);
});
it('should call dispatch on store with correct parameter if selected agent was changed', async () => {
spyOn(store, 'dispatch');
await (
await loader.getHarness(MatSelectHarness)
).clickOptions({
text: 'HAHR Agent'
});
submittingTrigger();
expect(store.dispatch).toHaveBeenCalledOnceWith(
new SearchByTermAiAction({
searchTerm: query,
agentId: '1'
})
);
});
it('should reset query input', () => {
spyOn(component.queryControl, 'reset');
submittingTrigger();
expect(component.queryControl.reset).toHaveBeenCalled();
});
it('should emit searchSubmitted event', () => {
spyOn(component.searchSubmitted, 'emit');
submittingTrigger();
expect(component.searchSubmitted.emit).toHaveBeenCalled();
});
it('should call open modal if there was a previous search phrase in url', () => {
submittingTrigger();
expect(dialogOpenSpy).toHaveBeenCalled();
});
it('should open Unsaved Changes Modal and run callback successfully', () => {
const modalAiSpy = spyOn(modalAiService, 'openUnsavedChangesModal').and.callThrough();
spyOn(component.searchSubmitted, 'emit');
fixture.detectChanges();
submittingTrigger();
expect(modalAiSpy).toHaveBeenCalledWith(jasmine.any(Function));
expect(component.searchSubmitted.emit).toHaveBeenCalled();
});
});
}
});