Files
portal/app/tests/Frontend/notifications-store.spec.ts
T

220 lines
7.6 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
// Мокаем api/notifications до import'а store.
vi.mock('../../resources/js/api/notifications', () => ({
listNotifications: vi.fn(),
markNotificationRead: vi.fn(),
markAllNotificationsRead: vi.fn(),
deleteNotification: vi.fn(),
}));
vi.mock('../../resources/js/api/client', () => ({
apiClient: {},
ensureCsrfCookie: vi.fn(),
extractValidationErrors: vi.fn(() => null),
extractErrorMessage: vi.fn(() => 'Произошла ошибка.'),
extractRateLimitRetry: vi.fn(() => null),
}));
import * as notificationsApi from '../../resources/js/api/notifications';
import { useNotificationsStore } from '../../resources/js/stores/notifications';
import type { ApiInAppNotification } from '../../resources/js/api/notifications';
const mockNotif = (id: number, overrides: Partial<ApiInAppNotification> = {}): ApiInAppNotification => ({
id,
event: 'new_lead',
title: `Уведомление #${id}`,
body: 'Тестовое тело',
deal_id: 100 + id,
payload: { deal_id: 100 + id, project_name: 'Caranga' },
read_at: null,
created_at: new Date(2026, 4, 9, 12, id).toISOString(),
...overrides,
});
describe('useNotificationsStore', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('initial state: items=[], unreadCount=0, total=0', () => {
const store = useNotificationsStore();
expect(store.items).toEqual([]);
expect(store.unreadCount).toBe(0);
expect(store.total).toBe(0);
expect(store.fetchError).toBe(false);
});
it('load() заполняет items + unreadCount + total из API', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1), mockNotif(2, { read_at: '2026-05-09T10:00:00Z' })],
unread_count: 1,
total: 2,
});
const store = useNotificationsStore();
await store.load();
expect(store.items).toHaveLength(2);
expect(store.unreadCount).toBe(1);
expect(store.total).toBe(2);
expect(store.fetchError).toBe(false);
});
it('load() при reject ставит fetchError=true, items не меняются', async () => {
vi.mocked(notificationsApi.listNotifications).mockRejectedValue(new Error('network'));
const store = useNotificationsStore();
await store.load();
expect(store.fetchError).toBe(true);
expect(store.items).toEqual([]);
});
it('markRead() optimistic ставит read_at + уменьшает unreadCount', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1)],
unread_count: 1,
total: 1,
});
vi.mocked(notificationsApi.markNotificationRead).mockResolvedValue({
id: 1,
read_at: '2026-05-09T13:00:00Z',
});
const store = useNotificationsStore();
await store.load();
expect(store.unreadCount).toBe(1);
await store.markRead(1);
expect(store.unreadCount).toBe(0);
expect(store.items[0]!.read_at).toBe('2026-05-09T13:00:00Z');
});
it('markRead() при reject — revert read_at + unreadCount', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1)],
unread_count: 1,
total: 1,
});
vi.mocked(notificationsApi.markNotificationRead).mockRejectedValue(new Error('500'));
const store = useNotificationsStore();
await store.load();
await store.markRead(1);
expect(store.unreadCount).toBe(1);
expect(store.items[0]!.read_at).toBeNull();
});
it('markRead() для уже прочитанного НЕ вызывает API', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1, { read_at: '2026-05-09T10:00:00Z' })],
unread_count: 0,
total: 1,
});
const store = useNotificationsStore();
await store.load();
await store.markRead(1);
expect(notificationsApi.markNotificationRead).not.toHaveBeenCalled();
});
it('markAllRead() optimistic + вызывает API', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1), mockNotif(2)],
unread_count: 2,
total: 2,
});
vi.mocked(notificationsApi.markAllNotificationsRead).mockResolvedValue({ updated: 2 });
const store = useNotificationsStore();
await store.load();
expect(store.unreadCount).toBe(2);
await store.markAllRead();
expect(store.unreadCount).toBe(0);
expect(store.items.every((n) => n.read_at !== null)).toBe(true);
expect(notificationsApi.markAllNotificationsRead).toHaveBeenCalledOnce();
});
it('markAllRead() при unreadCount=0 НЕ вызывает API', async () => {
const store = useNotificationsStore();
await store.markAllRead();
expect(notificationsApi.markAllNotificationsRead).not.toHaveBeenCalled();
});
it('remove() optimistic убирает из items + decrement total', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1), mockNotif(2)],
unread_count: 2,
total: 2,
});
vi.mocked(notificationsApi.deleteNotification).mockResolvedValue();
const store = useNotificationsStore();
await store.load();
await store.remove(1);
expect(store.items).toHaveLength(1);
expect(store.items[0]!.id).toBe(2);
expect(store.total).toBe(1);
expect(store.unreadCount).toBe(1);
});
it('remove() при reject — revert items + total + unreadCount', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1), mockNotif(2)],
unread_count: 2,
total: 2,
});
vi.mocked(notificationsApi.deleteNotification).mockRejectedValue(new Error('500'));
const store = useNotificationsStore();
await store.load();
await store.remove(1);
expect(store.items).toHaveLength(2);
expect(store.total).toBe(2);
expect(store.unreadCount).toBe(2);
});
it('sortedItems сортирует по created_at DESC', async () => {
const earlier = mockNotif(1, { created_at: '2026-05-09T10:00:00Z' });
const later = mockNotif(2, { created_at: '2026-05-09T15:00:00Z' });
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [earlier, later],
unread_count: 2,
total: 2,
});
const store = useNotificationsStore();
await store.load();
expect(store.sortedItems[0]!.id).toBe(2); // later first
expect(store.sortedItems[1]!.id).toBe(1);
});
it('reset() очищает state', async () => {
vi.mocked(notificationsApi.listNotifications).mockResolvedValue({
items: [mockNotif(1)],
unread_count: 1,
total: 1,
});
const store = useNotificationsStore();
await store.load();
store.reset();
expect(store.items).toEqual([]);
expect(store.unreadCount).toBe(0);
expect(store.total).toBe(0);
expect(store.fetchError).toBe(false);
});
});