});
describe('interceptor error handling', () => {
+ const expectSaveToHaveBeenCalled = (called) => {
+ tick(510);
+ if (called) {
+ expect(notificationService.save).toHaveBeenCalled();
+ } else {
+ expect(notificationService.save).not.toHaveBeenCalled();
+ }
+ };
+
it('should show default behaviour', fakeAsync(() => {
httpError(undefined, { status: 500 });
- tick(10);
- expect(notificationService.save).toHaveBeenCalled();
+ expectSaveToHaveBeenCalled(true);
}));
it('should prevent the default behaviour with preventDefault', fakeAsync(() => {
httpError(undefined, { status: 500 }, (resp) => resp.preventDefault());
- tick(10);
- expect(notificationService.save).not.toHaveBeenCalled();
+ expectSaveToHaveBeenCalled(false);
}));
it('should be able to use preventDefault with 400 errors', fakeAsync(() => {
{ status: 400 },
(resp) => resp.preventDefault()
);
- tick(10);
- expect(notificationService.save).not.toHaveBeenCalled();
+ expectSaveToHaveBeenCalled(false);
}));
it('should prevent the default behaviour by status code', fakeAsync(() => {
httpError(undefined, { status: 500 }, (resp) => resp.ignoreStatusCode(500));
- tick(10);
- expect(notificationService.save).not.toHaveBeenCalled();
+ expectSaveToHaveBeenCalled(false);
}));
it('should use different application icon (default Ceph) in error message', fakeAsync(() => {
httpError(undefined, { status: 500 }, (resp) => {
(resp.application = 'Prometheus'), (resp.message = msg);
});
- tick(10);
+ expectSaveToHaveBeenCalled(true);
expect(notificationService.save).toHaveBeenCalledWith(
createCdNotification(0, '500 - Unknown Error', msg, undefined, 'Prometheus')
);
expect(service['dataSource'].getValue().length).toBe(0);
}));
- it('should create a success notification and save it', fakeAsync(() => {
- service.show(new CdNotificationConfig(NotificationType.success, 'Simple test'));
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(1);
- expect(service['dataSource'].getValue()[0].type).toBe(NotificationType.success);
- }));
+ describe('Saved notifications', () => {
+ const expectSavedNotificationToHave = (expected: {}) => {
+ tick(510);
+ expect(service['dataSource'].getValue().length).toBe(1);
+ const notification = service['dataSource'].getValue()[0];
+ Object.keys(expected).forEach((key) => {
+ expect(notification[key]).toBe(expected[key]);
+ });
+ };
- it('should create an error notification and save it', fakeAsync(() => {
- service.show(NotificationType.error, 'Simple test');
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(1);
- expect(service['dataSource'].getValue()[0].type).toBe(NotificationType.error);
- }));
+ beforeEach(() => {
+ service.cancel(service['justShownTimeoutId']);
+ });
- it('should create an info notification and save it', fakeAsync(() => {
- service.show(new CdNotificationConfig(NotificationType.info, 'Simple test'));
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(1);
- const notification = service['dataSource'].getValue()[0];
- expect(notification.type).toBe(NotificationType.info);
- expect(notification.title).toBe('Simple test');
- expect(notification.message).toBe(undefined);
- }));
+ it('should create a success notification and save it', fakeAsync(() => {
+ service.show(new CdNotificationConfig(NotificationType.success, 'Simple test'));
+ expectSavedNotificationToHave({ type: NotificationType.success });
+ }));
- it('should never have more then 10 notifications', fakeAsync(() => {
- for (let index = 0; index < 15; index++) {
- service.show(NotificationType.info, 'Simple test');
- tick(100);
- }
- expect(service['dataSource'].getValue().length).toBe(10);
- }));
+ it('should create an error notification and save it', fakeAsync(() => {
+ service.show(NotificationType.error, 'Simple test');
+ expectSavedNotificationToHave({ type: NotificationType.error });
+ }));
- it('should show a success task notification', fakeAsync(() => {
- const task = _.assign(new FinishedTask(), {
- success: true
- });
- service.notifyTask(task, true);
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(1);
- const notification = service['dataSource'].getValue()[0];
- expect(notification.type).toBe(NotificationType.success);
- expect(notification.title).toBe('Executed unknown task');
- expect(notification.message).toBe(undefined);
- }));
+ it('should create an info notification and save it', fakeAsync(() => {
+ service.show(new CdNotificationConfig(NotificationType.info, 'Simple test'));
+ expectSavedNotificationToHave({
+ type: NotificationType.info,
+ title: 'Simple test',
+ message: undefined
+ });
+ }));
- it('should be able to stop notifyTask from notifying', fakeAsync(() => {
- const task = _.assign(new FinishedTask(), {
- success: true
- });
- const timeoutId = service.notifyTask(task, true);
- service.cancel(timeoutId);
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(0);
- }));
+ it('should never have more then 10 notifications', fakeAsync(() => {
+ for (let index = 0; index < 15; index++) {
+ service.show(NotificationType.info, 'Simple test');
+ tick(510);
+ }
+ expect(service['dataSource'].getValue().length).toBe(10);
+ }));
+
+ it('should show a success task notification', fakeAsync(() => {
+ const task = _.assign(new FinishedTask(), {
+ success: true
+ });
+ service.notifyTask(task, true);
+ expectSavedNotificationToHave({
+ type: NotificationType.success,
+ title: 'Executed unknown task',
+ message: undefined
+ });
+ }));
- it('should show a error task notification', fakeAsync(() => {
- const task = _.assign(
- new FinishedTask('rbd/create', {
- pool_name: 'somePool',
- image_name: 'someImage'
- }),
- {
- success: false,
- exception: {
- code: 17
+ it('should be able to stop notifyTask from notifying', fakeAsync(() => {
+ const task = _.assign(new FinishedTask(), {
+ success: true
+ });
+ const timeoutId = service.notifyTask(task, true);
+ service.cancel(timeoutId);
+ tick(100);
+ expect(service['dataSource'].getValue().length).toBe(0);
+ }));
+
+ it('should show a error task notification', fakeAsync(() => {
+ const task = _.assign(
+ new FinishedTask('rbd/create', {
+ pool_name: 'somePool',
+ image_name: 'someImage'
+ }),
+ {
+ success: false,
+ exception: {
+ code: 17
+ }
}
- }
- );
- service.notifyTask(task);
- tick(100);
- expect(service['dataSource'].getValue().length).toBe(1);
- const notification = service['dataSource'].getValue()[0];
- expect(notification.type).toBe(NotificationType.error);
- expect(notification.title).toBe(`Failed to create RBD 'somePool/someImage'`);
- expect(notification.message).toBe(`Name is already used by RBD 'somePool/someImage'.`);
- }));
+ );
+ service.notifyTask(task);
+ expectSavedNotificationToHave({
+ type: NotificationType.error,
+ title: `Failed to create RBD 'somePool/someImage'`,
+ message: `Name is already used by RBD 'somePool/someImage'.`
+ });
+ }));
+
+ it('combines different notifications with the same title', fakeAsync(() => {
+ service.show(NotificationType.error, '502 - Bad Gateway', 'Error occurred in path a');
+ tick(60);
+ service.show(NotificationType.error, '502 - Bad Gateway', 'Error occurred in path b');
+ expectSavedNotificationToHave({
+ type: NotificationType.error,
+ title: '502 - Bad Gateway',
+ message: '<ul><li>Error occurred in path a</li><li>Error occurred in path b</li></ul>'
+ });
+ }));
+ });
describe('notification queue', () => {
const n1 = new CdNotificationConfig(NotificationType.success, 'Some success');
const n2 = new CdNotificationConfig(NotificationType.info, 'Some info');
+ const showArray = (arr) => arr.forEach((n) => service.show(n));
+
beforeEach(() => {
- spyOn(service, 'show').and.stub();
+ spyOn(service, 'save').and.stub();
});
it('filters out duplicated notifications on single call', fakeAsync(() => {
- service.queueNotifications([n1, n1, n2, n2]);
- tick(500);
- expect(service.show).toHaveBeenCalledTimes(2);
+ showArray([n1, n1, n2, n2]);
+ tick(510);
+ expect(service.save).toHaveBeenCalledTimes(2);
}));
it('filters out duplicated notifications presented in different calls', fakeAsync(() => {
- service.queueNotifications([n1, n2]);
- service.queueNotifications([n1, n2]);
- tick(500);
- expect(service.show).toHaveBeenCalledTimes(2);
+ showArray([n1, n2]);
+ showArray([n1, n2]);
+ tick(1000);
+ expect(service.save).toHaveBeenCalledTimes(2);
}));
it('will reset the timeout on every call', fakeAsync(() => {
- service.queueNotifications([n1, n2]);
- tick(400);
- service.queueNotifications([n1, n2]);
- tick(100);
- expect(service.show).toHaveBeenCalledTimes(0);
- tick(400);
- expect(service.show).toHaveBeenCalledTimes(2);
+ showArray([n1, n2]);
+ tick(490);
+ showArray([n1, n2]);
+ tick(450);
+ expect(service.save).toHaveBeenCalledTimes(0);
+ tick(60);
+ expect(service.save).toHaveBeenCalledTimes(2);
}));
it('wont filter out duplicated notifications if timeout was reached before', fakeAsync(() => {
- service.queueNotifications([n1, n2]);
- tick(500);
- service.queueNotifications([n1, n2]);
- tick(500);
- expect(service.show).toHaveBeenCalledTimes(4);
+ showArray([n1, n2]);
+ tick(510);
+ showArray([n1, n2]);
+ tick(510);
+ expect(service.save).toHaveBeenCalledTimes(4);
}));
});
// Observable sources
private dataSource = new BehaviorSubject<CdNotification[]>([]);
- private queuedNotifications: CdNotificationConfig[] = [];
// Observable streams
data$ = this.dataSource.asObservable();
- private queueTimeoutId: number;
+ private queued: CdNotificationConfig[] = [];
+ private queuedTimeoutId: number;
KEY = 'cdNotifications';
constructor(
localStorage.setItem(this.KEY, JSON.stringify(recent));
}
- queueNotifications(notifications: CdNotificationConfig[]) {
- this.queuedNotifications = this.queuedNotifications.concat(notifications);
- this.cancel(this.queueTimeoutId);
- this.queueTimeoutId = window.setTimeout(() => {
- this.sendQueuedNotifications();
- }, 500);
- }
-
- private sendQueuedNotifications() {
- _.uniqWith(this.queuedNotifications, _.isEqual).forEach((notification) => {
- this.show(notification);
- });
- this.queuedNotifications = [];
- }
-
/**
* Method for showing a notification.
* @param {NotificationType} type toastr type
application
);
}
+ this.queueToShow(config);
+ }, 10);
+ }
+
+ private queueToShow(config: CdNotificationConfig) {
+ this.cancel(this.queuedTimeoutId);
+ if (!this.queued.find((c) => _.isEqual(c, config))) {
+ this.queued.push(config);
+ }
+ this.queuedTimeoutId = window.setTimeout(() => {
+ this.showQueued();
+ }, 500);
+ }
+
+ private showQueued() {
+ this.getUnifiedTitleQueue().forEach((config) => {
const notification = new CdNotification(config);
this.save(notification);
this.showToasty(notification);
- }, 10);
+ });
+ }
+
+ private getUnifiedTitleQueue(): CdNotificationConfig[] {
+ return Object.values(this.queueShiftByTitle()).map((configs) => {
+ const config = configs[0];
+ if (configs.length > 1) {
+ config.message = '<ul>' + configs.map((c) => `<li>${c.message}</li>`).join('') + '</ul>';
+ }
+ return config;
+ });
+ }
+
+ private queueShiftByTitle(): { [key: string]: CdNotificationConfig[] } {
+ const byTitle: { [key: string]: CdNotificationConfig[] } = {};
+ let config: CdNotificationConfig;
+ while ((config = this.queued.shift())) {
+ if (!byTitle[config.title]) {
+ byTitle[config.title] = [];
+ }
+ byTitle[config.title].push(config);
+ }
+ return byTitle;
}
private showToasty(notification: CdNotification) {
prometheus = new PrometheusHelper();
service = TestBed.get(PrometheusAlertFormatter);
notificationService = TestBed.get(NotificationService);
- spyOn(notificationService, 'queueNotifications').and.stub();
+ spyOn(notificationService, 'show').and.stub();
});
it('should create', () => {
describe('sendNotifications', () => {
it('should not call queue notifications with no notification', () => {
service.sendNotifications([]);
- expect(notificationService.queueNotifications).not.toHaveBeenCalled();
+ expect(notificationService.show).not.toHaveBeenCalled();
});
it('should call queue notifications with notifications', () => {
const notifications = [new CdNotificationConfig(NotificationType.success, 'test')];
service.sendNotifications(notifications);
- expect(notificationService.queueNotifications).toHaveBeenCalledWith(notifications);
+ expect(notificationService.show).toHaveBeenCalledWith(notifications[0]);
});
});
constructor(private notificationService: NotificationService) {}
sendNotifications(notifications: CdNotificationConfig[]) {
- if (notifications.length > 0) {
- this.notificationService.queueNotifications(notifications);
- }
+ notifications.forEach((n) => this.notificationService.show(n));
}
convertToCustomAlerts(
spyOn(window, 'setTimeout').and.callFake((fn: Function) => fn());
notificationService = TestBed.get(NotificationService);
- spyOn(notificationService, 'queueNotifications').and.callThrough();
spyOn(notificationService, 'show').and.stub();
prometheusService = TestBed.get(PrometheusService);
it('should notify on alert change', () => {
alerts = [prometheus.createAlert('alert0', 'suppressed')];
service.refresh();
- expect(notificationService.queueNotifications).toHaveBeenCalledWith([
+ expect(notificationService.show).toHaveBeenCalledWith(
new CdNotificationConfig(
NotificationType.info,
'alert0 (suppressed)',
undefined,
'Prometheus'
)
- ]);
+ );
});
it('should notify on a new alert', () => {
service.refresh();
alerts = [alert1, prometheus.createAlert('alert2')];
service.refresh();
- expect(notificationService.queueNotifications).toHaveBeenCalledWith([
- new CdNotificationConfig(
- NotificationType.error,
- 'alert2 (active)',
- 'alert2 is active ' + prometheus.createLink('http://alert2'),
- undefined,
- 'Prometheus'
- ),
- new CdNotificationConfig(
- NotificationType.success,
- 'alert0 (resolved)',
- 'alert0 is active ' + prometheus.createLink('http://alert0'),
- undefined,
- 'Prometheus'
- )
- ]);
+ expect(notificationService.show).toHaveBeenCalledTimes(2);
});
});
});
import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { TestBed } from '@angular/core/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { ToastModule } from 'ng2-toastr';
+import { ToastModule, ToastsManager } from 'ng2-toastr';
import { of } from 'rxjs';
import {
let prometheus: PrometheusHelper;
let shown: CdNotificationConfig[];
+ const toastFakeService = {
+ error: () => true,
+ info: () => true,
+ success: () => true
+ };
+
configureTestBed({
imports: [ToastModule.forRoot(), SharedModule, HttpClientTestingModule],
- providers: [PrometheusNotificationService, PrometheusAlertFormatter, i18nProviders]
+ providers: [
+ PrometheusNotificationService,
+ PrometheusAlertFormatter,
+ i18nProviders,
+ { provide: ToastsManager, useValue: toastFakeService }
+ ]
});
beforeEach(() => {
service['notifications'] = [];
notificationService = TestBed.get(NotificationService);
- spyOn(notificationService, 'queueNotifications').and.callThrough();
shown = [];
- spyOn(notificationService, 'show').and.callFake((n) => shown.push(n));
+ spyOn(notificationService, 'show').and.callThrough();
+ spyOn(notificationService, 'save').and.callFake((n) => shown.push(n));
spyOn(window, 'setTimeout').and.callFake((fn: Function) => fn());
});
describe('looks of fired notifications', () => {
- beforeEach(() => {
+ const asyncRefresh = () => {
service.refresh();
+ tick(20);
+ };
+
+ const expectShown = (expected: {}[]) => {
+ tick(500);
+ expect(shown.length).toBe(expected.length);
+ expected.forEach((e, i) =>
+ Object.keys(e).forEach((key) => expect(shown[i][key]).toEqual(expected[i][key]))
+ );
+ };
+
+ beforeEach(() => {
service.refresh();
- shown = [];
});
it('notifies on the second call', () => {
+ service.refresh();
expect(notificationService.show).toHaveBeenCalledTimes(1);
});
- it('notify looks on single notification with single alert like', () => {
- expect(notificationService.queueNotifications).toHaveBeenCalledWith([
+ it('notify looks on single notification with single alert like', fakeAsync(() => {
+ asyncRefresh();
+ expectShown([
new CdNotificationConfig(
NotificationType.error,
'alert0 (active)',
'Prometheus'
)
]);
- });
+ }));
- it('raises multiple pop overs for a single notification with multiple alerts', () => {
+ it('raises multiple pop overs for a single notification with multiple alerts', fakeAsync(() => {
+ asyncRefresh();
notifications[0].alerts.push(prometheus.createNotificationAlert('alert1', 'resolved'));
- service.refresh();
- expect(shown).toEqual([
+ asyncRefresh();
+ expectShown([
new CdNotificationConfig(
NotificationType.error,
'alert0 (active)',
'Prometheus'
)
]);
- });
+ }));
- it('should raise multiple notifications if they do not look like each other', () => {
+ it('should raise multiple notifications if they do not look like each other', fakeAsync(() => {
notifications[0].alerts.push(prometheus.createNotificationAlert('alert1'));
notifications.push(prometheus.createNotification());
notifications[1].alerts.push(prometheus.createNotificationAlert('alert2'));
- service.refresh();
- expect(shown).toEqual([
+ asyncRefresh();
+ expectShown([
new CdNotificationConfig(
NotificationType.error,
'alert0 (active)',
'Prometheus'
)
]);
- });
+ }));
it('only shows toasties if it got new data', () => {
+ service.refresh();
expect(notificationService.show).toHaveBeenCalledTimes(1);
notifications = [];
service.refresh();
expect(notificationService.show).toHaveBeenCalledTimes(3);
});
- it('filters out duplicated and non user visible changes in notifications', () => {
+ it('filters out duplicated and non user visible changes in notifications', fakeAsync(() => {
// Return 2 notifications with 3 duplicated alerts and 1 non visible changed alert
const secondAlert = prometheus.createNotificationAlert('alert0');
secondAlert.endsAt = new Date().toString(); // Should be ignored as it's not visible
notifications.push(prometheus.createNotification());
notifications[1].alerts.push(prometheus.createNotificationAlert('alert0'));
notifications[1].notified = 'by somebody else';
- service.refresh();
+ asyncRefresh();
- expect(shown).toEqual([
+ expectShown([
new CdNotificationConfig(
NotificationType.error,
'alert0 (active)',
'Prometheus'
)
]);
- });
+ }));
});
});
color: #333;
font-size: 1.1em;
}
+
+.toast-message > ul {
+ padding-left: 1em;
+ margin: 0;
+}