// @vitest-environment jsdom import { act, createElement } from 'react'; import { createRoot } from 'react-dom/client'; import { describe, expect, it, vi } from 'vitest'; import type { MonitorSummary } from '@uptime/contracts'; import { MonitorList } from './monitor-list.js'; function summary(id: string, name: string): MonitorSummary { return { monitor: { id, name, url: `https://${name.toLowerCase()}.example.test`, regionIds: ['us-east'], intervalSeconds: 300, timeoutMs: 10_000, enabled: true, dnsDiagnosticsEnabled: false, isPublic: false, publicSlug: null, createdAt: '2026-09-03T10:00:00.000Z', updatedAt: '2026-09-03T10:00:00.000Z', }, status: 'up', latestByRegion: {} as MonitorSummary['latestByRegion'], targetChecksPerDay: 288, }; } const bravo = summary('00000000-0000-4000-8000-000000000002', 'Bravo'); const items: MonitorSummary[] = [ summary('00000000-0000-4000-8000-000000000001', 'Alpha'), { ...bravo, monitor: { ...bravo.monitor, regionIds: ['eu-west'], badge: { id: '00000000-0000-4000-8000-000000000003', name: 'API', color: '#2563eb', }, }, }, ]; describe('bulk monitor editing', () => { it('updates the frequency for the selected monitors', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ updatedCount: 2 }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const reactEnvironment = globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }; const previousActEnvironment = reactEnvironment.IS_REACT_ACT_ENVIRONMENT; reactEnvironment.IS_REACT_ACT_ENVIRONMENT = true; const changed = vi.fn(); const container = document.createElement('div'); const root = createRoot(container); await act(async () => root.render(createElement(MonitorList, { items, onChanged: changed }))); const buttons = [...container.querySelectorAll('button')]; await act(async () => buttons.find((button) => button.textContent === 'Edit multiple')?.click(), ); const selectAll = container.querySelector( '.bulk-monitor-editor__select-all input', )!; await act(async () => selectAll.click()); const frequency = container.querySelector('select')!; await act(async () => { frequency.value = '1200'; frequency.dispatchEvent(new Event('change', { bubbles: true })); }); const apply = [...container.querySelectorAll('button')].find((button) => button.textContent?.startsWith('Apply to'), )!; await act(async () => apply.click()); expect(fetchMock).toHaveBeenCalledWith( '/api/monitors/bulk-frequency', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ monitorIds: items.map((item) => item.monitor.id), intervalSeconds: 1_200, }), }), ); expect(changed).toHaveBeenCalledOnce(); await act(async () => root.unmount()); vi.unstubAllGlobals(); if (previousActEnvironment === undefined) delete reactEnvironment.IS_REACT_ACT_ENVIRONMENT; else reactEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; }); it('selects the visible range between checked monitors with shift-click', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue( new Response(JSON.stringify({ badges: [] }), { status: 200, headers: { 'content-type': 'application/json' }, }), ), ); const reactEnvironment = globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }; const previousActEnvironment = reactEnvironment.IS_REACT_ACT_ENVIRONMENT; reactEnvironment.IS_REACT_ACT_ENVIRONMENT = true; const container = document.createElement('div'); const root = createRoot(container); const rangeItems = [ summary('00000000-0000-4000-8000-000000000011', 'Alpha'), summary('00000000-0000-4000-8000-000000000012', 'Bravo'), summary('00000000-0000-4000-8000-000000000013', 'Charlie'), ]; await act(async () => { root.render(createElement(MonitorList, { items: rangeItems, onChanged: () => undefined })); await Promise.resolve(); }); await act(async () => [...container.querySelectorAll('button')] .find((button) => button.textContent === 'Edit multiple') ?.click(), ); const checkboxes = container.querySelectorAll('.monitor-row__select'); await act(async () => checkboxes[0]!.click()); await act(async () => checkboxes[2]!.dispatchEvent(new MouseEvent('click', { bubbles: true, shiftKey: true })), ); expect([...checkboxes].map((checkbox) => checkbox.checked)).toEqual([true, true, true]); await act(async () => root.unmount()); vi.unstubAllGlobals(); if (previousActEnvironment === undefined) delete reactEnvironment.IS_REACT_ACT_ENVIRONMENT; else reactEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; }); }); describe('monitor filtering', () => { it('filters by name, badge, and region', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ badges: [items[1]!.monitor.badge] }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const reactEnvironment = globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT?: boolean }; const previousActEnvironment = reactEnvironment.IS_REACT_ACT_ENVIRONMENT; reactEnvironment.IS_REACT_ACT_ENVIRONMENT = true; const container = document.createElement('div'); const root = createRoot(container); await act(async () => { root.render(createElement(MonitorList, { items, onChanged: () => undefined })); await Promise.resolve(); await Promise.resolve(); }); const name = container.querySelector('.monitor-filters input')!; await act(async () => { name.value = 'bravo'; name.dispatchEvent(new Event('change', { bubbles: true })); }); expect(container.querySelectorAll('.monitor-row')).toHaveLength(1); await act(async () => { name.value = ''; name.dispatchEvent(new Event('change', { bubbles: true })); const selects = container.querySelectorAll('.monitor-filters select'); selects[0]!.value = items[1]!.monitor.badge!.id; selects[0]!.dispatchEvent(new Event('change', { bubbles: true })); selects[1]!.value = 'eu-west'; selects[1]!.dispatchEvent(new Event('change', { bubbles: true })); }); expect(container.querySelectorAll('.monitor-row')).toHaveLength(1); expect(container.querySelector('.monitor-row strong')?.textContent).toBe('Bravo'); await act(async () => root.unmount()); vi.unstubAllGlobals(); if (previousActEnvironment === undefined) delete reactEnvironment.IS_REACT_ACT_ENVIRONMENT; else reactEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment; }); });