web/storagenode: notifications domain and app types separations

WHAT:
notifications store, api, types refactoring;
service creation;
notifications module store tests creation;

WHY:
to separate domain and app types;
to be able to reuse code more easier;

Change-Id: I01c6584fc41bbf73e0b6f84501cc66bbebd50ace
This commit is contained in:
NickolaiYurchenko 2020-10-05 21:09:34 +03:00 committed by Nikolay Yurchenko
parent 074784e1b4
commit 3b388c21cf
13 changed files with 368 additions and 139 deletions

View File

@ -68,7 +68,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
import { NODE_ACTIONS } from '@/app/store/modules/node'; import { NODE_ACTIONS } from '@/app/store/modules/node';
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications'; import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout'; import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
import { NotificationsCursor } from '@/app/types/notifications';
const { const {
GET_NODE_INFO, GET_NODE_INFO,
@ -90,18 +89,23 @@ const {
export default class SNOHeader extends Vue { export default class SNOHeader extends Vue {
public isNotificationPopupShown: boolean = false; public isNotificationPopupShown: boolean = false;
public isOptionsShown: boolean = false; public isOptionsShown: boolean = false;
private readonly FIRST_PAGE: number = 1;
/** /**
* Lifecycle hook before render. * Lifecycle hook before render.
* Fetches first page of notifications. * Fetches first page of notifications.
*/ */
public beforeMount(): void { public async beforeMount(): Promise<void> {
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, true);
try { try {
this.$store.dispatch(NODE_ACTIONS.GET_NODE_INFO); await this.$store.dispatch(NODE_ACTIONS.GET_NODE_INFO);
this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1)); await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, this.FIRST_PAGE);
} catch (error) { } catch (error) {
console.error(error.message); console.error(error.message);
} }
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, false);
} }
public get nodeId(): string { public get nodeId(): string {
@ -192,7 +196,7 @@ export default class SNOHeader extends Vue {
} }
try { try {
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1)); await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, this.FIRST_PAGE);
} catch (error) { } catch (error) {
console.error(error.message); console.error(error.message);
} }

View File

@ -12,10 +12,10 @@
<div <div
class="notification-popup-container__content" class="notification-popup-container__content"
:class="{'collapsed': isCollapsed}" :class="{'collapsed': isCollapsed}"
v-if="latestNotifications.length" v-if="latest.length"
> >
<SNONotification <SNONotification
v-for="notification in latestNotifications" v-for="notification in latest"
:key="notification.id" :key="notification.id"
is-small="true" is-small="true"
:notification="notification" :notification="notification"
@ -34,6 +34,7 @@ import { Component, Vue } from 'vue-property-decorator';
import SNONotification from '@/app/components/notifications/SNONotification.vue'; import SNONotification from '@/app/components/notifications/SNONotification.vue';
import { RouteConfig } from '@/app/router'; import { RouteConfig } from '@/app/router';
import { UINotification } from '@/app/types/notifications';
@Component({ @Component({
components: { components: {
@ -49,7 +50,7 @@ export default class NotificationsPopup extends Vue {
/** /**
* Represents first page of notifications. * Represents first page of notifications.
*/ */
public get latestNotifications(): Notification[] { public get latest(): UINotification[] {
return this.$store.state.notificationsModule.latestNotifications; return this.$store.state.notificationsModule.latestNotifications;
} }
@ -57,7 +58,7 @@ export default class NotificationsPopup extends Vue {
* Indicates if popup is smaller than with scroll. * Indicates if popup is smaller than with scroll.
*/ */
public get isCollapsed(): boolean { public get isCollapsed(): boolean {
return this.latestNotifications.length < 4; return this.latest.length < 4;
} }
} }
</script> </script>

View File

@ -30,12 +30,12 @@
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications'; import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
import { Notification } from '@/app/types/notifications'; import { UINotification } from '@/app/types/notifications';
@Component @Component
export default class SNONotification extends Vue { export default class SNONotification extends Vue {
@Prop({default: () => new Notification()}) @Prop({default: () => new UINotification()})
public readonly notification: Notification; public readonly notification: UINotification;
/** /**
* isSmall props indicates if component used in popup. * isSmall props indicates if component used in popup.

View File

@ -4,17 +4,19 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { makeNotificationsModule } from '@/app/store/modules/notifications'; import { newNotificationsModule } from '@/app/store/modules/notifications';
import { makePayoutModule } from '@/app/store/modules/payout'; import { makePayoutModule } from '@/app/store/modules/payout';
import { NotificationsHttpApi } from '@/storagenode/api/notifications'; import { NotificationsHttpApi } from '@/storagenode/api/notifications';
import { PayoutHttpApi } from '@/storagenode/api/payout'; import { PayoutHttpApi } from '@/storagenode/api/payout';
import { SNOApi } from '@/storagenode/api/storagenode'; import { SNOApi } from '@/storagenode/api/storagenode';
import { NotificationsService } from '@/storagenode/notifications/service';
import { PayoutService } from '@/storagenode/payouts/service'; import { PayoutService } from '@/storagenode/payouts/service';
import { appStateModule } from './modules/appState'; import { appStateModule } from './modules/appState';
import { makeNodeModule } from './modules/node'; import { makeNodeModule } from './modules/node';
const notificationsApi = new NotificationsHttpApi(); const notificationsApi = new NotificationsHttpApi();
const notificationsService = new NotificationsService(notificationsApi);
const payoutApi = new PayoutHttpApi(); const payoutApi = new PayoutHttpApi();
const payoutService = new PayoutService(payoutApi); const payoutService = new PayoutService(payoutApi);
const nodeApi = new SNOApi(); const nodeApi = new SNOApi();
@ -28,7 +30,7 @@ export const store = new Vuex.Store({
modules: { modules: {
node: makeNodeModule(nodeApi), node: makeNodeModule(nodeApi),
appStateModule, appStateModule,
notificationsModule: makeNotificationsModule(notificationsApi), notificationsModule: newNotificationsModule(notificationsService),
payoutModule: makePayoutModule(payoutApi, payoutService), payoutModule: makePayoutModule(payoutApi, payoutService),
}, },
}); });

View File

@ -1,12 +1,8 @@
// Copyright (C) 2019 Storj Labs, Inc. // Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { import { NotificationsState, UINotification } from '@/app/types/notifications';
Notification, import { NotificationsService } from '@/storagenode/notifications/service';
NotificationsApi,
NotificationsCursor,
NotificationsState,
} from '@/app/types/notifications';
export const NOTIFICATIONS_MUTATIONS = { export const NOTIFICATIONS_MUTATIONS = {
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS', SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
@ -24,22 +20,22 @@ export const NOTIFICATIONS_ACTIONS = {
/** /**
* creates notifications module with all dependencies * creates notifications module with all dependencies
* *
* @param api - payments api * @param service - payments service
*/ */
export function makeNotificationsModule(api: NotificationsApi) { export function newNotificationsModule(service: NotificationsService) {
return { return {
state: new NotificationsState(), state: new NotificationsState(),
mutations: { mutations: {
[NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS](state: NotificationsState, notificationsResponse: NotificationsState): void { [NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS](state: NotificationsState, notificationsState: NotificationsState): void {
state.notifications = notificationsResponse.notifications; state.notifications = notificationsState.notifications;
state.pageCount = notificationsResponse.pageCount; state.pageCount = notificationsState.pageCount;
state.unreadCount = notificationsResponse.unreadCount; state.unreadCount = notificationsState.unreadCount;
}, },
[NOTIFICATIONS_MUTATIONS.SET_LATEST](state: NotificationsState, notificationsResponse: NotificationsState): void { [NOTIFICATIONS_MUTATIONS.SET_LATEST](state: NotificationsState, notificationsState: NotificationsState): void {
state.latestNotifications = notificationsResponse.notifications; state.latestNotifications = notificationsState.notifications;
}, },
[NOTIFICATIONS_MUTATIONS.MARK_AS_READ](state: NotificationsState, id: string): void { [NOTIFICATIONS_MUTATIONS.MARK_AS_READ](state: NotificationsState, id: string): void {
state.notifications = state.notifications.map((notification: Notification) => { state.notifications = state.notifications.map((notification: UINotification) => {
if (notification.id === id) { if (notification.id === id) {
notification.markAsRead(); notification.markAsRead();
} }
@ -48,7 +44,7 @@ export function makeNotificationsModule(api: NotificationsApi) {
}); });
}, },
[NOTIFICATIONS_MUTATIONS.READ_ALL](state: NotificationsState): void { [NOTIFICATIONS_MUTATIONS.READ_ALL](state: NotificationsState): void {
state.notifications = state.notifications.map((notification: Notification) => { state.notifications = state.notifications.map((notification: UINotification) => {
notification.markAsRead(); notification.markAsRead();
return notification; return notification;
@ -58,24 +54,26 @@ export function makeNotificationsModule(api: NotificationsApi) {
}, },
}, },
actions: { actions: {
[NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS]: async function ({commit}: any, cursor: NotificationsCursor): Promise<NotificationsState> { [NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS]: async function ({commit}: any, pageIndex: number): Promise<void> {
const notificationsResponse = await api.get(cursor); const notificationsResponse = await service.notifications(pageIndex);
commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationsResponse); const notifications = notificationsResponse.page.notifications.map(notification => new UINotification(notification));
if (cursor.page === 1) { const notificationState = new NotificationsState(notifications, notificationsResponse.page.pageCount, notificationsResponse.unreadCount);
commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationsResponse);
commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationState);
if (pageIndex === 1) {
commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationState);
} }
return notificationsResponse;
}, },
[NOTIFICATIONS_ACTIONS.MARK_AS_READ]: async function ({commit}: any, id: string): Promise<any> { [NOTIFICATIONS_ACTIONS.MARK_AS_READ]: async function ({commit}: any, id: string): Promise<void> {
await api.read(id); await service.readSingeNotification(id);
commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, id); commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, id);
}, },
[NOTIFICATIONS_ACTIONS.READ_ALL]: async function ({commit}: any): Promise<any> { [NOTIFICATIONS_ACTIONS.READ_ALL]: async function ({commit}: any): Promise<void> {
await api.readAll(); await service.readAllNotifications();
commit(NOTIFICATIONS_MUTATIONS.READ_ALL); commit(NOTIFICATIONS_MUTATIONS.READ_ALL);
}, },

View File

@ -2,23 +2,39 @@
// See LICENSE for copying information. // See LICENSE for copying information.
import { NotificationIcon } from '@/app/utils/notificationIcons'; import { NotificationIcon } from '@/app/utils/notificationIcons';
import { Notification, NotificationTypes } from '@/storagenode/notifications/notifications';
/**
* Holds all notifications module state.
*/
export class NotificationsState {
public latestNotifications: UINotification[] = [];
public constructor(
public notifications: UINotification[] = [],
public pageCount: number = 0,
public unreadCount: number = 0,
) { }
}
/** /**
* Describes notification entity. * Describes notification entity.
*/ */
export class Notification { export class UINotification {
public icon: NotificationIcon; public icon: NotificationIcon;
public isRead: boolean;
public id: string;
public senderId: string;
public type: NotificationTypes;
public title: string;
public message: string;
public readAt: Date | null;
public createdAt: Date;
public constructor( public constructor(notification: Partial<UINotification> = new Notification()) {
public id: string = '', Object.assign(this, notification);
public senderId: string = '',
public type: NotificationTypes = NotificationTypes.Custom,
public title: string = '',
public message: string = '',
public isRead: boolean = false,
public createdAt: Date = new Date(),
) {
this.setIcon(); this.setIcon();
this.isRead = !!this.readAt;
} }
/** /**
@ -70,60 +86,3 @@ export class Notification {
} }
} }
} }
/**
* Describes all current notifications types.
*/
export enum NotificationTypes {
Custom = 0,
AuditCheckFailure = 1,
UptimeCheckFailure = 2,
Disqualification = 3,
Suspension = 4,
}
/**
* Describes page offset for pagination.
*/
export class NotificationsCursor {
public constructor(
public page: number = 0,
public limit: number = 7,
) { }
}
/**
* Holds all notifications module state.
*/
export class NotificationsState {
public latestNotifications: Notification[] = [];
public constructor(
public notifications: Notification[] = [],
public pageCount: number = 0,
public unreadCount: number = 0,
) { }
}
/**
* Exposes all notifications-related functionality.
*/
export interface NotificationsApi {
/**
* Fetches notifications.
* @throws Error
*/
get(cursor: NotificationsCursor): Promise<NotificationsState>;
/**
* Marks single notification as read.
* @throws Error
*/
read(id: string): Promise<void>;
/**
* Marks all notification as read.
* @throws Error
*/
readAll(): Promise<void>;
}

View File

@ -20,7 +20,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
import { NODE_ACTIONS } from '@/app/store/modules/node'; import { NODE_ACTIONS } from '@/app/store/modules/node';
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications'; import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout'; import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
import { NotificationsCursor } from '@/app/types/notifications';
@Component ({ @Component ({
components: { components: {
@ -43,7 +42,7 @@ export default class Dashboard extends Vue {
} }
try { try {
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1)); await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -54,7 +54,7 @@ import VPagination from '@/app/components/VPagination.vue';
import BackArrowIcon from '@/../static/images/notifications/backArrow.svg'; import BackArrowIcon from '@/../static/images/notifications/backArrow.svg';
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications'; import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
import { Notification, NotificationsCursor } from '@/app/types/notifications'; import { UINotification } from '@/app/types/notifications';
@Component ({ @Component ({
components: { components: {
@ -67,7 +67,7 @@ export default class NotificationsArea extends Vue {
/** /**
* Returns notification of current page. * Returns notification of current page.
*/ */
public get notifications(): Notification[] { public get notifications(): UINotification[] {
return this.$store.state.notificationsModule.notifications; return this.$store.state.notificationsModule.notifications;
} }
@ -92,7 +92,7 @@ export default class NotificationsArea extends Vue {
*/ */
public async onPageClick(index: number): Promise<void> { public async onPageClick(index: number): Promise<void> {
try { try {
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(index)); await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, index);
} catch (error) { } catch (error) {
console.error(error.message); console.error(error.message);
} }

View File

@ -57,7 +57,6 @@ import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
import { NODE_ACTIONS } from '@/app/store/modules/node'; import { NODE_ACTIONS } from '@/app/store/modules/node';
import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications'; import { NOTIFICATIONS_ACTIONS } from '@/app/store/modules/notifications';
import { PAYOUT_ACTIONS } from '@/app/store/modules/payout'; import { PAYOUT_ACTIONS } from '@/app/store/modules/payout';
import { NotificationsCursor } from '@/app/types/notifications';
import { PayoutPeriod, TotalHeldAndPaid } from '@/storagenode/payouts/payouts'; import { PayoutPeriod, TotalHeldAndPaid } from '@/storagenode/payouts/payouts';
@Component ({ @Component ({
@ -88,7 +87,7 @@ export default class PayoutArea extends Vue {
} }
try { try {
await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, new NotificationsCursor(1)); await this.$store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -1,7 +1,12 @@
// Copyright (C) 2019 Storj Labs, Inc. // Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { Notification, NotificationsApi, NotificationsCursor, NotificationsState } from '@/app/types/notifications'; import {
NotificationsApi,
NotificationsCursor,
NotificationsPage,
NotificationsResponse,
} from '@/storagenode/notifications/notifications';
import { HttpClient } from '@/storagenode/utils/httpClient'; import { HttpClient } from '@/storagenode/utils/httpClient';
/** /**
@ -15,10 +20,10 @@ export class NotificationsHttpApi implements NotificationsApi {
/** /**
* Fetch notifications. * Fetch notifications.
* *
* @returns notifications state * @returns notifications response.
* @throws Error * @throws Error
*/ */
public async get(cursor: NotificationsCursor): Promise<NotificationsState> { public async get(cursor: NotificationsCursor): Promise<NotificationsResponse> {
const path = `${this.ROOT_PATH}/list?page=${cursor.page}&limit=${cursor.limit}`; const path = `${this.ROOT_PATH}/list?page=${cursor.page}&limit=${cursor.limit}`;
const response = await this.client.get(path); const response = await this.client.get(path);
@ -27,28 +32,12 @@ export class NotificationsHttpApi implements NotificationsApi {
} }
const notificationResponse = await response.json(); const notificationResponse = await response.json();
let notifications: Notification[] = [];
let pageCount: number = 0;
let unreadCount: number = 0;
if (notificationResponse) { return new NotificationsResponse(
notifications = notificationResponse.page.notifications.map(item => new NotificationsPage(notificationResponse.page.notifications, notificationResponse.page.pageCount),
new Notification( notificationResponse.unreadCount,
item.id, notificationResponse.totalCount,
item.senderId,
item.type,
item.title,
item.message,
!!item.readAt,
new Date(item.createdAt),
),
); );
pageCount = notificationResponse.page.pageCount;
unreadCount = notificationResponse.unreadCount;
}
return new NotificationsState(notifications, pageCount, unreadCount);
} }
/** /**

View File

@ -0,0 +1,88 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* Exposes all notifications-related functionality.
*/
export interface NotificationsApi {
/**
* Fetches notifications.
* @throws Error
*/
get(cursor: NotificationsCursor): Promise<NotificationsResponse>;
/**
* Marks single notification as read.
* @throws Error
*/
read(id: string): Promise<void>;
/**
* Marks all notification as read.
* @throws Error
*/
readAll(): Promise<void>;
}
/**
* Describes notification entity.
*/
export class Notification {
public constructor(
public id: string = '',
public senderId: string = '',
public type: NotificationTypes = NotificationTypes.Custom,
public title: string = '',
public message: string = '',
public readAt: Date | null = null,
public createdAt: Date = new Date(),
) {}
}
/**
* Describes all current notifications types.
*/
export enum NotificationTypes {
Custom = 0,
AuditCheckFailure = 1,
UptimeCheckFailure = 2,
Disqualification = 3,
Suspension = 4,
}
/**
* Describes page offset for pagination.
*/
export class NotificationsCursor {
private DEFAULT_LIMIT: number = 7;
public constructor(
public page: number = 0,
public limit: number = 0,
) {
if (!this.limit) {
this.limit = this.DEFAULT_LIMIT;
}
}
}
/**
* Describes response object from server.
*/
export class NotificationsResponse {
public constructor(
public page: NotificationsPage = new NotificationsPage(),
public unreadCount: number = 0,
public totalCount: number = 0,
) {}
}
/**
* Describes page related notification information.
*/
export class NotificationsPage {
public constructor(
public notifications: Notification[] = [],
public pageCount: number = 0,
) {}
}

View File

@ -0,0 +1,43 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import { NotificationsApi, NotificationsCursor, NotificationsResponse } from '@/storagenode/notifications/notifications';
/**
* PayoutService is used to store and handle node paystub information.
* PayoutService exposes a business logic related to payouts.
*/
export class NotificationsService {
private readonly api: NotificationsApi;
public constructor(api: NotificationsApi) {
this.api = api;
}
/**
* Fetch notifications.
*
* @returns notifications response.
* @throws Error
*/
public async notifications(index: number, limit?: number): Promise<NotificationsResponse> {
const cursor = new NotificationsCursor(index, limit);
return await this.api.get(cursor);
}
/**
* Marks single notification as read on server.
* @param id
*/
public async readSingeNotification(id: string): Promise<void> {
await this.api.read(id);
}
/**
* Marks all notifications as read on server.
*/
public async readAllNotifications(): Promise<void> {
await this.api.readAll();
}
}

View File

@ -0,0 +1,147 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import { newNotificationsModule, NOTIFICATIONS_ACTIONS, NOTIFICATIONS_MUTATIONS } from '@/app/store/modules/notifications';
import { NotificationsState, UINotification } from '@/app/types/notifications';
import { NotificationsHttpApi } from '@/storagenode/api/notifications';
import {
Notification,
NotificationsPage,
NotificationsResponse,
NotificationTypes,
} from '@/storagenode/notifications/notifications';
import { NotificationsService } from '@/storagenode/notifications/service';
import { createLocalVue } from '@vue/test-utils';
const Vue = createLocalVue();
const notificationsApi = new NotificationsHttpApi();
const notificationsService = new NotificationsService(notificationsApi);
const notificationsModule = newNotificationsModule(notificationsService);
Vue.use(Vuex);
const store = new Vuex.Store({ modules: { notificationsModule } });
const state = store.state as any;
let notifications;
describe('mutations', () => {
beforeEach(() => {
createLocalVue().use(Vuex);
notifications = [
new UINotification(new Notification('1', '1', NotificationTypes.Disqualification, 'title1', 'message1', null)),
new UINotification(new Notification('2', '1', NotificationTypes.UptimeCheckFailure, 'title2', 'message2', null)),
];
});
it('sets notification state', (): void => {
const notificationsState = new NotificationsState(notifications, 2, 1);
store.commit(NOTIFICATIONS_MUTATIONS.SET_NOTIFICATIONS, notificationsState);
expect(state.notificationsModule.notifications.length).toBe(notifications.length);
expect(state.notificationsModule.pageCount).toBe(2);
expect(state.notificationsModule.unreadCount).toBe(1);
store.commit(NOTIFICATIONS_MUTATIONS.SET_LATEST, notificationsState);
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
});
it('sets single notification as read', (): void => {
store.commit(NOTIFICATIONS_MUTATIONS.MARK_AS_READ, '1');
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(1);
});
it('sets all notification as read', (): void => {
store.commit(NOTIFICATIONS_MUTATIONS.READ_ALL);
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(0);
});
});
describe('actions', () => {
beforeEach(() => {
jest.resetAllMocks();
notifications = [
new UINotification(new Notification('1', '1', NotificationTypes.Disqualification, 'title1', 'message1', null)),
new UINotification(new Notification('2', '1', NotificationTypes.UptimeCheckFailure, 'title2', 'message2', null)),
];
});
it('throws error on failed notifications fetch', async (): Promise<void> => {
jest.spyOn(notificationsApi, 'get').mockImplementation(() => { throw new Error(); });
try {
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
expect(true).toBe(false);
} catch (error) {
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
}
});
it('success fetches notifications', async (): Promise<void> => {
jest.spyOn(notificationsService, 'notifications')
.mockReturnValue(Promise.resolve(new NotificationsResponse(new NotificationsPage(notifications, 1), 2, 1)));
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
expect(state.notificationsModule.latestNotifications.length).toBe(notifications.length);
});
it('throws error on failed single notification read', async (): Promise<void> => {
jest.spyOn(notificationsApi, 'read').mockImplementation(() => { throw new Error(); });
try {
await store.dispatch(NOTIFICATIONS_ACTIONS.MARK_AS_READ, '1');
expect(true).toBe(false);
} catch (error) {
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(notifications.length);
}
});
it('success marks single notification as read', async (): Promise<void> => {
jest.spyOn(notificationsService, 'notifications')
.mockReturnValue(Promise.resolve(new NotificationsResponse(new NotificationsPage(notifications, 1), 2, 1)));
await store.dispatch(NOTIFICATIONS_ACTIONS.GET_NOTIFICATIONS, 1);
await store.dispatch(NOTIFICATIONS_ACTIONS.MARK_AS_READ, '1');
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(1);
});
it('throws error on failed all notifications read', async (): Promise<void> => {
jest.spyOn(notificationsApi, 'readAll').mockImplementation(() => { throw new Error(); });
try {
await store.dispatch(NOTIFICATIONS_ACTIONS.READ_ALL);
expect(true).toBe(false);
} catch (error) {
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(1);
}
});
it('success marks all notifications as read', async (): Promise<void> => {
await store.dispatch(NOTIFICATIONS_ACTIONS.READ_ALL);
const unreadNotificationsCount = state.notificationsModule.notifications.filter(e => !e.isRead).length;
expect(unreadNotificationsCount).toBe(0);
});
});