220 lines
7.6 KiB
TypeScript
220 lines
7.6 KiB
TypeScript
|
|
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);
|
|||
|
|
});
|
|||
|
|
});
|