[ACA-4299] Add e2e tests for task counters and notifications (#6726)

* [ACA-4299] Add ete tests for task counters and notifications

* Fix unit test

* Fix update for indirect counter changes
This commit is contained in:
davidcanonieto
2021-02-26 08:57:27 +01:00
committed by GitHub
parent f3c4680c2c
commit e96491fe25
15 changed files with 349 additions and 94 deletions

View File

@@ -219,7 +219,8 @@
"NOTIFICATIONS": {
"TASK_ASSIGNED": "{{taskName}} task has been assigned to {{assignee}}",
"PROCESS_STARTED": "{{processName}} process has been started",
"TASK_UPDATED": "{{taskName}} task details has been updated"
"TASK_UPDATED": "{{taskName}} task details have been updated",
"TASK_CREATED": "{{taskName}} task was created"
},
"FORM-LOADING": {
"FORM_DATA": "Form Data",

View File

@@ -16,7 +16,7 @@
*/
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ChartsModule } from 'ng2-charts';
@@ -112,6 +112,8 @@ import localePl from '@angular/common/locales/pl';
import localeFi from '@angular/common/locales/fi';
import localeDa from '@angular/common/locales/da';
import localeSv from '@angular/common/locales/sv';
import { setupAppNotifications } from './services/app-notifications-factory';
import { AppNotificationsService } from './services/app-notifications.service';
registerLocaleData(localeFr);
registerLocaleData(localeDe);
@@ -225,6 +227,13 @@ registerLocaleData(localeSv);
name: 'lazy-loading',
source: 'resources/lazy-loading'
}
},
AppNotificationsService,
{
provide: APP_INITIALIZER,
useFactory: setupAppNotifications,
deps: [AppNotificationsService],
multi: true
}
],
bootstrap: [AppComponent]

View File

@@ -17,23 +17,6 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { CloudLayoutService } from './services/cloud-layout.service';
import { NotificationModel, NotificationService } from '@alfresco/adf-core';
import { map } from 'rxjs/operators';
import { NotificationCloudService } from '@alfresco/adf-process-services-cloud';
import { TranslateService } from '@ngx-translate/core';
const SUBSCRIPTION_QUERY = `
subscription {
engineEvents(eventType: [
PROCESS_STARTED
TASK_ASSIGNED
TASK_UPDATED
]) {
eventType
entity
}
}
`;
@Component({
selector: 'app-cloud-layout',
@@ -48,26 +31,13 @@ export class CloudLayoutComponent implements OnInit {
constructor(
private router: Router,
private route: ActivatedRoute,
private cloudLayoutService: CloudLayoutService,
private notificationCloudService: NotificationCloudService,
private notificationService: NotificationService,
private translateService: TranslateService
private cloudLayoutService: CloudLayoutService
) { }
ngOnInit() {
let root: string = '';
this.route.params.subscribe((params) => {
this.appName = params.appName;
this.notificationCloudService.makeGQLQuery(
this.appName, SUBSCRIPTION_QUERY
)
.pipe(map((events: any) => events.data.engineEvents))
.subscribe((result) => {
result.map((engineEvent) => {
this.notifyEvent(engineEvent);
});
});
});
if (this.route.snapshot && this.route.snapshot.firstChild) {
@@ -92,37 +62,4 @@ export class CloudLayoutComponent implements OnInit {
onStartProcess() {
this.router.navigate([`/cloud/${this.appName}/start-process/`]);
}
notifyEvent(engineEvent) {
let message;
switch (engineEvent.eventType) {
case 'TASK_ASSIGNED':
message = this.translateService.instant('NOTIFICATIONS.TASK_ASSIGNED',
{ taskName: engineEvent.entity.name || '', assignee: engineEvent.entity.assignee });
this.pushNotification(engineEvent, message);
break;
case 'PROCESS_STARTED':
message = this.translateService.instant('NOTIFICATIONS.PROCESS_STARTED',
{ processName: engineEvent.entity.name });
this.pushNotification(engineEvent, message);
break;
case 'TASK_UPDATED':
message = this.translateService.instant('NOTIFICATIONS.TASK_UPDATED',
{ taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
default:
}
}
pushNotification(engineEvent: any, message: string) {
const notification = {
messages: [message],
icon: 'info',
datetime: new Date(),
initiator: { displayName: engineEvent.entity.initiator || 'System' }
} as NotificationModel;
this.notificationService.pushToNotificationHistory(notification);
}
}

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed 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 { AppNotificationsService } from './app-notifications.service';
export function setupAppNotifications(appNotificationsService: AppNotificationsService) {
return () => appNotificationsService;
}

View File

@@ -0,0 +1,110 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed 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 { AppConfigService, NotificationService, NotificationModel, AlfrescoApiService, IdentityUserService } from '@alfresco/adf-core';
import { NotificationCloudService } from '@alfresco/adf-process-services-cloud';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { map } from 'rxjs/operators';
const SUBSCRIPTION_QUERY = `
subscription {
engineEvents(eventType: [
PROCESS_STARTED
TASK_ASSIGNED
TASK_UPDATED,
TASK_CREATED
]) {
eventType
entity
}
}
`;
@Injectable()
export class AppNotificationsService {
constructor(
private appConfigService: AppConfigService,
private notificationCloudService: NotificationCloudService,
private notificationService: NotificationService,
private translateService: TranslateService,
private identityUserService: IdentityUserService,
private alfrescoApiService: AlfrescoApiService
) {
this.alfrescoApiService.alfrescoApiInitialized.subscribe(() => {
const deployedApps = this.appConfigService.get('alfresco-deployed-apps', []);
if (deployedApps?.length) {
deployedApps.forEach((app) => {
this.notificationCloudService
.makeGQLQuery(app.name, SUBSCRIPTION_QUERY)
.pipe(map((events: any) => events.data.engineEvents))
.subscribe((result) => {
result.map((engineEvent) => this.notifyEvent(engineEvent));
});
});
}
});
}
notifyEvent(engineEvent) {
let message;
switch (engineEvent.eventType) {
case 'TASK_ASSIGNED':
message = this.translateService.instant('NOTIFICATIONS.TASK_ASSIGNED', { taskName: engineEvent.entity.name || '', assignee: engineEvent.entity.assignee });
this.pushNotification(engineEvent, message);
break;
case 'TASK_UPDATED':
message = this.translateService.instant('NOTIFICATIONS.TASK_UPDATED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
case 'TASK_COMPLETED':
message = this.translateService.instant('NOTIFICATIONS.TASK_COMPLETED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
case 'TASK_ACTIVATED':
message = this.translateService.instant('NOTIFICATIONS.TASK_ACTIVATED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
case 'TASK_CANCELLED':
message = this.translateService.instant('NOTIFICATIONS.TASK_CANCELLED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
case 'TASK_SUSPENDED':
message = this.translateService.instant('NOTIFICATIONS.TASK_SUSPENDED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
case 'TASK_CREATED':
message = this.translateService.instant('NOTIFICATIONS.TASK_CREATED', { taskName: engineEvent.entity.name || '' });
this.pushNotification(engineEvent, message);
break;
default:
}
}
pushNotification(engineEvent: any, message: string) {
if (engineEvent.entity.assignee === this.identityUserService.getCurrentUserInfo().username) {
const notification = {
messages: [message],
icon: 'info',
datetime: new Date(),
initiator: { displayName: engineEvent.entity.initiator || 'System' }
} as NotificationModel;
this.notificationService.pushToNotificationHistory(notification);
}
}
}

View File

@@ -0,0 +1,128 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed 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 { browser } from 'protractor';
import {
LoginPage,
TasksService,
ApiService,
AppListCloudPage,
StringUtil,
IdentityService,
GroupIdentityService,
NotificationHistoryPage,
ProcessInstancesService,
ProcessDefinitionsService,
QueryService
} from '@alfresco/adf-testing';
import { NavigationBarPage } from '../core/pages/navigation-bar.page';
import { TasksCloudDemoPage } from './pages/tasks-cloud-demo.page';
describe('Task counters cloud', () => {
describe('Task Counters', () => {
const simpleApp = browser.params.resources.ACTIVITI_CLOUD_APPS.SIMPLE_APP.name;
const loginSSOPage = new LoginPage();
const navigationBarPage = new NavigationBarPage();
const appListCloudComponent = new AppListCloudPage();
const tasksCloudDemoPage = new TasksCloudDemoPage();
const notificationHistoryPage = new NotificationHistoryPage();
const taskFilter = tasksCloudDemoPage.taskFilterCloudComponent;
const apiService = new ApiService();
const identityService = new IdentityService(apiService);
const groupIdentityService = new GroupIdentityService(apiService);
const tasksService = new TasksService(apiService);
const processDefinitionService = new ProcessDefinitionsService(apiService);
const processInstancesService = new ProcessInstancesService(apiService);
const queryService = new QueryService(apiService);
let testUser, groupInfo;
const createdTaskName = StringUtil.generateRandomString();
beforeAll(async () => {
await apiService.loginWithProfile('identityAdmin');
testUser = await identityService.createIdentityUserWithRole([identityService.ROLES.ACTIVITI_USER]);
groupInfo = await groupIdentityService.getGroupInfoByGroupName('hr');
await identityService.addUserToGroup(testUser.idIdentityService, groupInfo.id);
await apiService.login(testUser.username, testUser.password);
await loginSSOPage.login(testUser.username, testUser.password);
});
afterAll(async () => {
await apiService.loginWithProfile('identityAdmin');
await identityService.deleteIdentityUser(testUser.idIdentityService);
});
beforeEach(async () => {
await navigationBarPage.navigateToProcessServicesCloudPage();
await appListCloudComponent.checkApsContainer();
await appListCloudComponent.goToApp(simpleApp);
});
it('[C593065] Should display notification in counter when process started', async () => {
await taskFilter.checkTaskFilterCounter('my-tasks');
await expect(await taskFilter.getTaskFilterCounter('my-tasks')).toBe('0');
const processDefinition = await processDefinitionService.getProcessDefinitionByName(browser.params.resources.ACTIVITI_CLOUD_APPS.SIMPLE_APP.processes.uploadSingleMultipleFiles, simpleApp);
const processInstance = await processInstancesService.createProcessInstance(processDefinition.entry.key, simpleApp, { 'name': StringUtil.generateRandomString() });
const task = await queryService.getProcessInstanceTasks(processInstance.entry.id, simpleApp);
await tasksService.claimTask(task.list.entries[0].entry.id, simpleApp);
await notificationHistoryPage.checkNotificationCenterHasNewNotifications();
await notificationHistoryPage.clickNotificationButton();
await notificationHistoryPage.checkNotificationIsPresent(`task has been assigned`);
await notificationHistoryPage.clickMarkAsRead();
await processInstancesService.deleteProcessInstance(processInstance.entry.id, simpleApp);
});
it('[C593066] Should display notification in counter when task assigned', async () => {
await taskFilter.checkTaskFilterCounter('my-tasks');
await expect(await taskFilter.getTaskFilterCounter('my-tasks')).toBe('0');
const taskCounter = await taskFilter.getTaskFilterCounter('my-tasks');
const assigneeTask = await tasksService.createStandaloneTask(createdTaskName, simpleApp);
await tasksService.claimTask(assigneeTask.entry.id, simpleApp);
await taskFilter.waitForNotification('my-tasks');
await expect(await taskFilter.getTaskFilterCounter('my-tasks')).toBe((parseInt(taskCounter, 10) + 1).toString());
await notificationHistoryPage.clickNotificationButton();
await notificationHistoryPage.clickMarkAsRead();
await tasksService.deleteTask(assigneeTask.entry.id, simpleApp);
});
it('[C290009] Should display notification in task center', async () => {
await taskFilter.checkTaskFilterCounter('my-tasks');
const assigneeTask = await tasksService.createStandaloneTask(createdTaskName, simpleApp);
await tasksService.claimTask(assigneeTask.entry.id, simpleApp);
await notificationHistoryPage.checkNotificationCenterHasNewNotifications();
await notificationHistoryPage.clickNotificationButton();
await notificationHistoryPage.checkNotificationIsPresent(`${assigneeTask.entry.name} task has been assigned`);
await notificationHistoryPage.clickMarkAsRead();
await tasksService.deleteTask(assigneeTask.entry.id, simpleApp);
});
});
});

View File

@@ -25,6 +25,11 @@ import { Apollo } from 'apollo-angular';
describe('NotificationCloudService', () => {
let service: NotificationCloudService;
let apollo: Apollo;
let apolloCreateSpy;
let apolloSubscribeSpy;
const useMock = {
subscribe() {}
};
const queryMock = `
subscription {
@@ -47,12 +52,13 @@ describe('NotificationCloudService', () => {
beforeEach(async(() => {
service = TestBed.inject(NotificationCloudService);
apollo = TestBed.inject(Apollo);
service.appsListening = [];
apolloCreateSpy = spyOn(apollo, 'createNamed');
apolloSubscribeSpy = spyOn(apollo, 'use').and.returnValue(useMock);
}));
it('should not create more than one websocket per app if it was already created', () => {
const apolloCreateSpy = spyOn(apollo, 'create');
const apolloSubscribeSpy = spyOn(apollo, 'subscribe');
service.makeGQLQuery('myAppName', queryMock);
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');
@@ -61,12 +67,20 @@ describe('NotificationCloudService', () => {
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');
service.makeGQLQuery('myAppName2', queryMock);
expect(apolloCreateSpy).toHaveBeenCalledTimes(1);
expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2);
});
it('should create new websocket if it is subscribing to new app', () => {
service.makeGQLQuery('myAppName', queryMock);
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');
service.makeGQLQuery('myOtherAppName', queryMock);
expect(service.appsListening.length).toBe(2);
expect(service.appsListening[0]).toBe('myAppName');
expect(service.appsListening[1]).toBe('myAppName2');
expect(service.appsListening[1]).toBe('myOtherAppName');
expect(apolloCreateSpy).toHaveBeenCalledTimes(2);
expect(apolloSubscribeSpy).toHaveBeenCalledTimes(3);
expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2);
});
});

View File

@@ -71,15 +71,15 @@ export class NotificationCloudService extends BaseCloudService {
httpLink
);
this.apollo.create(<any> {
this.apollo.createNamed(appName, {
link,
cache: new InMemoryCache({})
cache: new InMemoryCache()
});
}
}
makeGQLQuery(appName: string, gqlQuery: string) {
this.initNotificationsForApp(appName);
return this.apollo.subscribe({ query : gql(gqlQuery) });
return this.apollo.use(appName).subscribe({ query: gql(gqlQuery) });
}
}

View File

@@ -383,8 +383,8 @@ describe('TaskFiltersCloudComponent', () => {
it('should update filter counter when notification received', async(() => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(fakeGlobalFilterObservable);
const change = new SimpleChange(undefined, 'my-app-1', true);
component.ngOnChanges({ 'appName': change });
component.appName = 'my-app-1';
component.ngOnInit();
fixture.detectChanges();
component.showIcons = true;
fixture.whenStable().then(() => {
@@ -399,7 +399,8 @@ describe('TaskFiltersCloudComponent', () => {
it('should reset filter counter notification when filter is selected', async(() => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(fakeGlobalFilterObservable);
let change = new SimpleChange(undefined, 'my-app-1', true);
component.ngOnChanges({ 'appName': change });
component.appName = 'my-app-1';
component.ngOnInit();
fixture.detectChanges();
component.showIcons = true;
fixture.whenStable().then(() => {

View File

@@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
import { TaskFilterCloudService } from '../services/task-filter-cloud.service';
import { TaskFilterCloudModel, FilterParamsModel } from '../models/filter-cloud.model';
import { TranslationService } from '@alfresco/adf-core';
import { map, takeUntil } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';
import { BaseTaskFiltersCloudComponent } from './base-task-filters-cloud.component';
import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model';
import { TaskCloudEngineEvent } from '../../../models/engine-event-cloud.model';
@@ -78,7 +78,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
this.resetFilter();
this.filters = Object.assign([], res);
this.selectFilterAndEmit(this.filterParam);
this.initFilterCounters();
this.updateFilterCounters();
this.success.emit(res);
},
(err: any) => {
@@ -87,7 +87,7 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
);
}
initFilterCounters() {
updateFilterCounters() {
this.filters.forEach((filter) => {
if (filter.showCounter) {
this.counters$[filter.key] = this.taskFilterCloudService.getTaskFilterCounter(filter);
@@ -96,27 +96,32 @@ export class TaskFiltersCloudComponent extends BaseTaskFiltersCloudComponent imp
}
initFilterCounterNotifications() {
this.taskFilterCloudService.getTaskNotificationSubscription(this.appName)
.subscribe((result: TaskCloudEngineEvent[]) => {
result.map((taskEvent: TaskCloudEngineEvent) => {
this.updateFilterCounter(taskEvent.entity);
if (this.appName) {
this.taskFilterCloudService.getTaskNotificationSubscription(this.appName)
.subscribe((result: TaskCloudEngineEvent[]) => {
result.map((taskEvent: TaskCloudEngineEvent) => {
this.checkFilterCounter(taskEvent.entity);
});
this.filterCounterUpdated.emit(result);
});
this.filterCounterUpdated.emit(result);
});
}
}
updateFilterCounter(filterNotification: TaskDetailsCloudModel) {
checkFilterCounter(filterNotification: TaskDetailsCloudModel) {
this.filters.map((filter) => {
if (this.isFilterPresent(filter, filterNotification)) {
this.counters$[filter.key] = this.counters$[filter.key].pipe(map((counter) => counter + 1));
this.addToUpdatedCounters(filter.key);
}
});
if (this.updatedCounters.length) {
this.updateFilterCounters();
}
}
isFilterPresent(filter: TaskFilterCloudModel, filterNotification: TaskDetailsCloudModel): boolean {
return filter.status === filterNotification.status
&& filter.assignee === filterNotification.assignee;
&& (filter.assignee === filterNotification.assignee || filterNotification.assignee === undefined);
}
public selectFilter(paramFilter: FilterParamsModel) {

View File

@@ -34,8 +34,8 @@ const TASK_EVENT_SUBSCRIPTION_QUERY = `
TASK_ASSIGNED
TASK_ACTIVATED
TASK_SUSPENDED
TASK_CANCELLED
TASK_UPDATED
TASK_CANCELLED,
TASK_CREATED
]) {
eventType
entity

View File

@@ -35,6 +35,11 @@ export class NotificationHistoryPage {
await BrowserVisibility.waitUntilElementHasText(this.notificationList, text);
}
async checkNotificationCenterHasNewNotifications(): Promise<void> {
const notificationListButton = element(by.css('#adf-notification-history-open-button [class*="mat-badge-active"]'));
await BrowserVisibility.waitUntilElementIsVisible(notificationListButton);
}
async checkNotificationIsNotPresent(text: string): Promise<void> {
const notificationLisText = await BrowserActions.getText(this.notificationList);
await expect(notificationLisText).not.toContain(text);

View File

@@ -49,8 +49,22 @@ export class TaskFiltersCloudComponentPage {
return BrowserActions.getText(this.activeFilter);
}
async getTaskFilterCounter(filterName: string): Promise<string> {
const filterCounter = element.all(by.css(`[data-automation-id="${filterName}_filter-counter"]`)).first();
return BrowserActions.getText(filterCounter);
}
async checkTaskFilterCounter(filterName: string): Promise<void> {
const filterCounter = element.all(by.css(`[data-automation-id="${filterName}_filter-counter"]`)).first();
await BrowserVisibility.waitUntilElementHasText(filterCounter, '0');
}
async waitForNotification(filterName: string): Promise<void> {
const filterCounter = element(by.css(`[data-automation-id="${filterName}_filter-counter"][class*="adf-active"]`));
await BrowserVisibility.waitUntilElementIsVisible(filterCounter);
}
getTaskFilterLocatorByFilterName(filterName: string): ElementFinder {
return element.all(by.css(`button[data-automation-id="${filterName}_filter"]`)).first();
}
}

8
package-lock.json generated
View File

@@ -4,6 +4,14 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@alfresco/adf-testing": {
"version": "4.3.0-31536",
"resolved": "https://registry.npmjs.org/@alfresco/adf-testing/-/adf-testing-4.3.0-31536.tgz",
"integrity": "sha512-V4hXMtQqFlV6h6rBN91H+6DNWRwa5MNXgd/iFPxvRUUolJZi1N1Ehk+XBLFjfzECZlgAhRcr0sjJP62JIQC4fg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@alfresco/js-api": {
"version": "4.3.0-3244",
"resolved": "https://registry.npmjs.org/@alfresco/js-api/-/js-api-4.3.0-3244.tgz",

View File

@@ -72,6 +72,7 @@
],
"dependencies": {
"@alfresco/js-api": "^4.3.0-3244",
"@alfresco/adf-testing": "^4.3.0-31536",
"@angular/animations": "^10.0.4",
"@angular/cdk": "10.1.3",
"@angular/common": "^10.0.4",