import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../../src/auth", () => ({ getAccessToken: () => "test.jwt", refreshTokens: vi.fn(async () => undefined), })); vi.mock("../../src/config", () => ({ env: { apiBaseUrl: "https://example.test" }, })); vi.mock("../../src/services", () => ({ dialogApi: { messages: vi.fn(async () => ({ items: [], next_cursor: null })) }, })); vi.mock("react-native", () => ({ AppState: { currentState: "active" }, })); vi.mock("expo-linking", () => ({ openURL: vi.fn(async () => undefined), })); import { NOTIFICATION_OUTAGE_MS, NOTIFICATION_POLL_INTERVAL_MS, NotificationRealtimeClient, } from "../../src/realtime"; import { actionDialogId, actionUrl, formatNotificationPrice, notificationIcon, notificationPalette, typeMap, } from "../../src/notification-presenter"; import type { NotificationType } from "../../src/types"; class MockWebSocket { static readonly OPEN = 1; static instances: MockWebSocket[] = []; readonly sent: string[] = []; readyState = MockWebSocket.OPEN; onopen?: () => void; onmessage?: (event: { data: string }) => void; onclose?: (event: { code: number }) => void; onerror?: () => void; constructor(readonly url: string, readonly protocols: string[]) { MockWebSocket.instances.push(this); } send(value: string) { this.sent.push(value); } close() {} } describe("notification catalog presentation", () => { it("использует neutral и bell для неизвестных значений", () => { expect(notificationPalette("future-token")).toEqual(notificationPalette("neutral")); expect(notificationIcon("future-icon")).toBe("bell"); expect(notificationIcon(null)).toBe("bell"); }); it("рендерит новый вид только по данным каталога", () => { const type: NotificationType = { code: "future_type", label: "Новый вид", color_token: "info", icon_code: "news", cta_text: "Открыть", cta_action: "open_detail", countable: true, contour: "P", button_primary: { code: "gotit", label: "Понятно" }, button_secondary: null, }; expect(typeMap([type]).get("future_type")).toEqual(type); expect(notificationPalette(type.color_token).accent).toBe("#2563eb"); }); it("форматирует цену в рублях и безопасно игнорирует мусор", () => { expect(formatNotificationPrice("1500")).toContain("1 500"); expect(formatNotificationPrice("not-a-number")).toBeNull(); }); it("читает результат CTA из backend state response", () => { const base = { notification_id: "notification-1", lifecycle_status: "active" as const, visibility: "visible" as const, is_read: true, close_reason: null, date_expired: null, unread_count: 0, }; expect(actionUrl({ ...base, result: { action: "open_url", url: "https://pay.test" } })) .toBe("https://pay.test"); expect(actionDialogId({ ...base, result: { action: "chat_message_sent", message: { message_id: "message-1", dialog_id: "dialog-1", sender_type: "client", content_kind: "text", text: "Тест", attachments: [], safety_status: "allowed", delivery_status: "delivered", created_at: "2026-07-27T12:00:00Z", }, }, })).toBe("dialog-1"); expect(actionUrl({ ...base, result: null })).toBeUndefined(); expect(actionDialogId({ ...base, result: null })).toBeUndefined(); }); }); describe("notification realtime degradation", () => { beforeEach(() => { vi.useFakeTimers(); MockWebSocket.instances = []; globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; }); afterEach(() => { vi.useRealTimers(); }); it("подписывается с notifications:true", () => { const client = new NotificationRealtimeClient(vi.fn(), vi.fn(async () => undefined), vi.fn()); client.start(); const socket = MockWebSocket.instances[0]!; socket.onopen?.(); expect(JSON.parse(socket.sent[0]!)).toEqual({ type: "subscribe", dialog_ids: [], notifications: true, }); client.stop(); }); it("после 30 секунд включает polling с интервалом 60 секунд", async () => { const reconcile = vi.fn(async () => undefined); const state = vi.fn(); const client = new NotificationRealtimeClient(vi.fn(), reconcile, state); client.start(); MockWebSocket.instances[0]!.onclose?.({ code: 1006 }); await vi.advanceTimersByTimeAsync(NOTIFICATION_OUTAGE_MS); expect(state).toHaveBeenCalledWith("polling"); expect(reconcile).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(NOTIFICATION_POLL_INTERVAL_MS); expect(reconcile).toHaveBeenCalledTimes(2); client.stop(); }); });