// @vitest-environment jsdom import '@testing-library/jest-dom/vitest'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { SettingsRoute } from '../../apps/dashboard/src/features/settings/settings-route.js'; const config = { rateLimit: { minDelayBetweenMessagesMs: 300_000, maxMessagesPerHour: 8, jitterMs: 60_000 }, browser: { dryRun: true }, reddit: { prioritizeRecentPosts: false, recentWindowMinutes: 60, maxReadyCandidates: 25, physiqueTargeting: { enabled: false, minScore: 1, maxScore: 10 } }, subreddits: [{ name: 'casualconversation', type: 'chat', weight: 2, tone: 'default' }], tones: ['default', 'dominant', 'submissive', 'playful', 'direct', 'gentle', 'kink-friendly', 'teasing'], }; function pushSettings(overrides: Record = {}) { return { enabled: false, serverUrl: 'https://ntfy.sh', topic: '', priority: 'default', events: { imageReceived: true, replyReceived: false, messageFailed: false, botStopped: false, rateLimited: false, sessionExpired: true, syncFailed: false, }, hasAuthToken: false, ...overrides, }; } let settings = pushSettings(); let saved: unknown[] = []; const server = setupServer( http.get('/api/config', () => HttpResponse.json(config)), http.get('/api/accounts', () => HttpResponse.json([])), http.get('/api/push-notifications', () => HttpResponse.json(settings)), http.put('/api/push-notifications', async ({ request }) => { const body = await request.json() as Record; saved.push(body); settings = pushSettings({ ...body, events: { ...settings.events, ...(body.events as object) } }); return HttpResponse.json(settings); }), ); function renderSettings() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); const router = createMemoryRouter([{ path: '/settings', element: }], { initialEntries: ['/settings?section=notifications'], }); return render( , ); } function panel() { return screen.getByRole('region', { name: 'Notifications' }); } beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); beforeEach(() => { settings = pushSettings(); saved = []; }); afterEach(() => { cleanup(); server.resetHandlers(); vi.restoreAllMocks(); }); afterAll(() => server.close()); describe('push notification settings', () => { it('shows the notifications section with every selectable event', async () => { renderSettings(); const section = await screen.findByRole('region', { name: 'Notifications' }); expect(within(section).getByLabelText(/Server URL/)).toHaveValue('https://ntfy.sh'); expect(within(section).getByRole('group', { name: /Events to notify me about \(2 of 7\)/ })).toBeInTheDocument(); expect(within(section).getByRole('checkbox', { name: /Image received/ })).toBeChecked(); expect(within(section).getByRole('checkbox', { name: /Reply received/ })).not.toBeChecked(); expect(within(section).getByText(/No access token is configured/)).toBeInTheDocument(); }); it('saves the topic and event selection the operator picked', async () => { const user = userEvent.setup(); renderSettings(); await screen.findByRole('region', { name: 'Notifications' }); await user.click(within(panel()).getByRole('checkbox', { name: /Send push notifications/ })); await user.type(within(panel()).getByLabelText(/Topic/), 'redditbot-7f3a9c21'); await user.click(within(panel()).getByRole('checkbox', { name: /Reply received/ })); await user.click(within(panel()).getByRole('button', { name: /Save notifications/ })); await waitFor(() => expect(saved).toHaveLength(1)); expect(saved[0]).toMatchObject({ enabled: true, topic: 'redditbot-7f3a9c21', events: { imageReceived: true, replyReceived: true }, }); expect(await within(panel()).findByText('Notifications saved.')).toBeInTheDocument(); }); it('refuses to save an enabled configuration with no topic', async () => { const user = userEvent.setup(); renderSettings(); await screen.findByRole('region', { name: 'Notifications' }); await user.click(within(panel()).getByRole('checkbox', { name: /Send push notifications/ })); await user.click(within(panel()).getByRole('button', { name: /Save notifications/ })); expect(within(panel()).getByText(/A topic is required/)).toBeInTheDocument(); expect(saved).toHaveLength(0); }); it('rejects a topic containing characters ntfy will not accept', async () => { const user = userEvent.setup(); renderSettings(); await screen.findByRole('region', { name: 'Notifications' }); await user.type(within(panel()).getByLabelText(/Topic/), 'not a topic!'); await user.click(within(panel()).getByRole('button', { name: /Save notifications/ })); expect(within(panel()).getByText(/only contain letters, numbers/)).toBeInTheDocument(); expect(saved).toHaveLength(0); }); it('sends a test notification and reports the delivery outcome', async () => { settings = pushSettings({ enabled: true, topic: 'redditbot-7f3a9c21' }); server.use(http.post('/api/push-notifications/test', () => HttpResponse.json({ delivered: false, reason: 'ntfy responded 403 Forbidden', }))); const user = userEvent.setup(); renderSettings(); await screen.findByRole('region', { name: 'Notifications' }); await user.click(within(panel()).getByRole('button', { name: /Send test/ })); expect(await within(panel()).findByText(/ntfy responded 403 Forbidden/)).toBeInTheDocument(); }); it('will not send a test until unsaved changes are saved', async () => { settings = pushSettings({ enabled: true, topic: 'redditbot-7f3a9c21' }); const user = userEvent.setup(); renderSettings(); await screen.findByRole('region', { name: 'Notifications' }); await user.click(within(panel()).getByRole('checkbox', { name: /Sync failed/ })); expect(within(panel()).getByRole('button', { name: /Send test/ })).toBeDisabled(); }); it('reports that an access token is configured without revealing it', async () => { settings = pushSettings({ hasAuthToken: true }); renderSettings(); const section = await screen.findByRole('region', { name: 'Notifications' }); expect(within(section).getByText(/An access token is configured/)).toBeInTheDocument(); }); });