import { beforeEach, describe, expect, it, vi } from 'vitest' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { EscalationsPage } from '../pages/EscalationsPage' import { ChannelsPage } from '../pages/ChannelsPage' import { session } from '../api/session' import { fetchRouter, res, type Call } from './support' /** * Канал, на який посилається сходинка драбини. * * ЧОГО ЦЕЙ ФАЙЛ БОЇТЬСЯ. Драбина зберігалась із будь-яким * ідентифікатором каналу, а видалення каналу не чіпало сходинок, що на * нього посилались. Ззовні це виглядало як робоче нічне чергування: * сходинка стоїть, хвилини стоять, у стовпчику каналів — прочерк, який * читається як «не заповнено» й нікого не турбує. Насправді ця * сходинка не йде нікуди й списується мовчки. * * У формі це давало ще й глухий кут: галочки для видаленого каналу в * переліку немає, тобто прибрати його руками неможливо, а сервер * (після виправлення) такої драбини вже не приймає. * * Форма підставних відповідей звірена з * `server/internal/httpapi/alerts.go` (`handleListEscalationPolicies`, * `handleListChannels`) і зі `store.EscalationPolicy` / `store.Channel`. */ const duty = { id: 'ch-duty', kind: 'telegram', name: 'Черговий', config: {}, min_severity: 'warning', enabled: true, has_secret: true, } /** Драбина, друга сходинка якої посилається на вже видалений канал. */ const policy = { id: 'p-1', name: 'Нічне чергування', description: '', steps: [ { after_min: 15, channel_ids: ['ch-duty'] }, { after_min: 45, channel_ids: ['ch-gone'] }, ], repeat_after_min: 0, max_repeats: 0, rule_count: 2, } beforeEach(() => { vi.stubGlobal('WebSocket', class {}) session.set('tok', { userID: 'u-me', username: 'me', tenantID: 't-1', permissions: ['alerts:read', 'alerts:write'], }) }) describe('сходинка, що посилається на видалений канал', () => { it('у переліку її видно словами, а не прочерком', async () => { fetchRouter({ 'GET /api/v1/escalation-policies': { policies: [policy] }, 'GET /api/v1/channels': { channels: [duty] }, }) render() await screen.findByText('Черговий') // Прочерк читався б як «нічого не налаштовано», тобто як спокійний // стан. Тут стан протилежний. expect(await screen.findByText('канал видалено')).toBeTruthy() }) it('форма прибирає привида, говорить про це і не шле його на сервер', async () => { const srv = fetchRouter({ 'GET /api/v1/escalation-policies': { policies: [policy] }, 'GET /api/v1/channels': { channels: [duty] }, 'PUT /api/v1/escalation-policies/p-1': () => res(200, { id: 'p-1' }), }) render() fireEvent.click(await screen.findByRole('button', { name: 'Змінити' })) await screen.findByText('Драбина: Нічне чергування') expect(screen.getByText(/лишались посилання на видалені канали/)).toBeTruthy() // Друга сходинка лишилась без каналу — і форма має сказати про це // ДО збереження, а не віддати серверу відмову без пояснення. expect(screen.getByText(/сходинка 2 не має жодного каналу/)).toBeTruthy() // Дали сходинці канал — тепер зберігається. // // Шукаємо саме галочку каналу, а не «останню на формі»: у формі є й // галочки, що каналів не стосуються (тиха година), і «остання» // мовчки перестала б робити те, заради чого цей рядок написано — // тест ловив би відсутність PUT замість зниклого каналу. fireEvent.click(screen.getAllByRole('checkbox', { name: /Черговий/ })[1]) fireEvent.click(screen.getByRole('button', { name: 'Зберегти' })) let put: Call | undefined await waitFor(() => { put = srv.calls.find((c) => c.method === 'PUT') expect(put).toBeTruthy() }) const body = put!.body as { steps: { channel_ids: string[] }[] } expect(body.steps.map((s) => s.channel_ids)).toEqual([['ch-duty'], ['ch-duty']]) }) }) describe('видалення каналу, на який спирається драбина', () => { it('називає драбини, які через це замовкнуть', async () => { fetchRouter({ 'GET /api/v1/channels': { channels: [{ ...duty, escalations: ['Нічне чергування', 'Вихідні'] }], }, }) render() fireEvent.click(await screen.findByRole('button', { name: 'Видалити' })) // Те саме, що вже показує видалення драбини: скільки чужих // налаштувань зараз перестане працювати, і яких саме. expect(await screen.findByText(/Нічне чергування, Вихідні/)).toBeTruthy() }) it('без драбин попередження про них не вигадується', async () => { fetchRouter({ 'GET /api/v1/channels': { channels: [duty] } }) render() fireEvent.click(await screen.findByRole('button', { name: 'Видалити' })) await screen.findByText(/більше не доставлятимуться/) expect(screen.queryByText(/драбини ескалації/)).toBeNull() }) })