mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2025-05-12 17:04:46 +00:00
* [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>
462 lines
16 KiB
TypeScript
462 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 { 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<AgentsButtonComponent>;
|
|
let agents$: Subject<Agent[]>;
|
|
let agentsMock: Agent[];
|
|
let checkSearchAvailabilitySpy: jasmine.Spy<(selectedNodesState: SelectionState, maxSelectedNodes?: number) => string>;
|
|
let selectionState: SelectionState;
|
|
let store: MockStore;
|
|
let config$: Subject<KnowledgeRetrievalConfigEntry>;
|
|
|
|
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<Agent[]>();
|
|
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<KnowledgeRetrievalConfigEntry>();
|
|
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<any>>;
|
|
|
|
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<MatSelectionListHarness> =>
|
|
(await loader.getHarness(MatMenuHarness)).getHarness(MatSelectionListHarness);
|
|
|
|
const selectAgent = async (): Promise<void> =>
|
|
(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<ToggleAISearchInput>).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');
|
|
});
|
|
});
|
|
});
|
|
});
|