TelegramVerdictMode.test.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. /**
  2. * Telegram outcome verdict mode (#3046).
  3. *
  4. * Coverage:
  5. * - AddNotificationModal shows the mode select only for Telegram providers.
  6. * - The select pre-fills from provider.telegram_verdict_mode and the chosen
  7. * value is submitted; non-Telegram providers always submit "buttons".
  8. * - NotificationProviderCard renders the reactions / both badge.
  9. */
  10. import { describe, it, expect, afterEach, vi } from 'vitest';
  11. import { screen, waitFor } from '@testing-library/react';
  12. import userEvent from '@testing-library/user-event';
  13. import { http, HttpResponse } from 'msw';
  14. import { render } from '../utils';
  15. import { server } from '../mocks/server';
  16. import { AddNotificationModal } from '../../components/AddNotificationModal';
  17. import { NotificationProviderCard } from '../../components/NotificationProviderCard';
  18. import type { NotificationProvider } from '../../api/client';
  19. afterEach(() => {
  20. server.resetHandlers();
  21. vi.restoreAllMocks();
  22. });
  23. function buildProvider(overrides: Partial<NotificationProvider> = {}): NotificationProvider {
  24. return {
  25. id: 1,
  26. name: 'My Telegram',
  27. provider_type: 'telegram',
  28. enabled: true,
  29. config: { bot_token: '123:abc', chat_id: '-100123' },
  30. on_print_start: false,
  31. on_print_complete: true,
  32. on_print_failed: true,
  33. on_print_stopped: true,
  34. on_print_progress: false,
  35. on_print_missing_spool_assignment: false,
  36. on_billing_charge_failed: true,
  37. on_printer_offline: false,
  38. on_printer_error: false,
  39. on_ai_failure_detection: false,
  40. on_filament_low: false,
  41. on_maintenance_due: false,
  42. on_ams_humidity_high: false,
  43. on_ams_temperature_high: false,
  44. on_ams_drying_suspended: true,
  45. on_ams_ht_humidity_high: false,
  46. on_ams_ht_temperature_high: false,
  47. on_plate_not_empty: true,
  48. on_plate_clear_required: false,
  49. on_print_confirm_request: true,
  50. telegram_verdict_mode: 'buttons',
  51. on_bed_cooled: false,
  52. on_ha_sensor_alert: false,
  53. on_location_ha_sensor_alert: false,
  54. on_first_layer_complete: false,
  55. on_queue_job_added: false,
  56. on_queue_job_assigned: false,
  57. on_queue_job_started: false,
  58. on_queue_job_waiting: true,
  59. on_queue_job_skipped: true,
  60. on_queue_job_failed: true,
  61. on_queue_completed: false,
  62. on_stock_reorder_alert: false,
  63. on_stock_break_alert: false,
  64. quiet_hours_enabled: false,
  65. quiet_hours_start: null,
  66. quiet_hours_end: null,
  67. daily_digest_enabled: false,
  68. daily_digest_time: null,
  69. printer_id: null,
  70. last_success: null,
  71. last_error: null,
  72. last_error_at: null,
  73. created_at: '2026-09-20T00:00:00Z',
  74. updated_at: '2026-09-20T00:00:00Z',
  75. ...overrides,
  76. };
  77. }
  78. describe('AddNotificationModal — Telegram verdict mode (#3046)', () => {
  79. it('shows the mode select for a Telegram provider, pre-filled from the provider', async () => {
  80. render(
  81. <AddNotificationModal provider={buildProvider({ telegram_verdict_mode: 'reactions' })} onClose={() => undefined} />,
  82. );
  83. const select = (await screen.findByLabelText('Outcome verdict via')) as HTMLSelectElement;
  84. expect(select.value).toBe('reactions');
  85. expect(screen.getByRole('option', { name: 'Inline buttons (link)' })).toBeInTheDocument();
  86. expect(screen.getByRole('option', { name: 'Reaction (👍 / 👎)' })).toBeInTheDocument();
  87. expect(screen.getByRole('option', { name: 'Buttons and reaction' })).toBeInTheDocument();
  88. expect(screen.getByText(/Reactions need no inbound connectivity/)).toBeInTheDocument();
  89. });
  90. it('does not show the mode select for other provider types', async () => {
  91. render(
  92. <AddNotificationModal
  93. provider={buildProvider({ provider_type: 'ntfy', config: { server: 'https://ntfy.sh', topic: 'x' } })}
  94. onClose={() => undefined}
  95. />,
  96. );
  97. await screen.findByDisplayValue('My Telegram');
  98. expect(screen.queryByLabelText('Outcome verdict via')).not.toBeInTheDocument();
  99. });
  100. it('submits the chosen mode on save', async () => {
  101. let captured: unknown = null;
  102. server.use(
  103. http.patch('*/api/v1/notifications/1', async ({ request }) => {
  104. captured = await request.json();
  105. return HttpResponse.json({ id: 1 });
  106. }),
  107. );
  108. const onClose = vi.fn();
  109. const user = userEvent.setup();
  110. render(<AddNotificationModal provider={buildProvider()} onClose={onClose} />);
  111. const select = await screen.findByLabelText('Outcome verdict via');
  112. await user.selectOptions(select, 'both');
  113. await user.click(screen.getByRole('button', { name: /^save$/i }));
  114. await waitFor(() => expect(onClose).toHaveBeenCalled());
  115. expect(captured).toMatchObject({ provider_type: 'telegram', telegram_verdict_mode: 'both' });
  116. });
  117. it('always submits "buttons" for a non-Telegram provider', async () => {
  118. let captured: unknown = null;
  119. server.use(
  120. http.patch('*/api/v1/notifications/1', async ({ request }) => {
  121. captured = await request.json();
  122. return HttpResponse.json({ id: 1 });
  123. }),
  124. );
  125. const onClose = vi.fn();
  126. const user = userEvent.setup();
  127. render(
  128. <AddNotificationModal
  129. provider={buildProvider({
  130. provider_type: 'ntfy',
  131. config: { server: 'https://ntfy.sh', topic: 'x' },
  132. telegram_verdict_mode: 'reactions',
  133. })}
  134. onClose={onClose}
  135. />,
  136. );
  137. await screen.findByDisplayValue('My Telegram');
  138. await user.click(screen.getByRole('button', { name: /^save$/i }));
  139. await waitFor(() => expect(onClose).toHaveBeenCalled());
  140. expect(captured).toMatchObject({ provider_type: 'ntfy', telegram_verdict_mode: 'buttons' });
  141. });
  142. });
  143. describe('NotificationProviderCard — Telegram verdict badge (#3046)', () => {
  144. it('shows the Reactions badge in reactions mode', async () => {
  145. render(<NotificationProviderCard provider={buildProvider({ telegram_verdict_mode: 'reactions' })} onEdit={vi.fn()} />);
  146. expect(await screen.findByText('Reactions')).toBeInTheDocument();
  147. expect(screen.queryByText('Buttons + reactions')).not.toBeInTheDocument();
  148. });
  149. it('shows the Buttons + reactions badge in both mode', async () => {
  150. render(<NotificationProviderCard provider={buildProvider({ telegram_verdict_mode: 'both' })} onEdit={vi.fn()} />);
  151. expect(await screen.findByText('Buttons + reactions')).toBeInTheDocument();
  152. });
  153. it('shows no mode badge in buttons mode or when the prompt event is muted', async () => {
  154. const { unmount } = render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  155. expect(await screen.findByText('Outcome Confirmation')).toBeInTheDocument();
  156. expect(screen.queryByText('Reactions')).not.toBeInTheDocument();
  157. unmount();
  158. render(
  159. <NotificationProviderCard
  160. provider={buildProvider({ telegram_verdict_mode: 'reactions', on_print_confirm_request: false })}
  161. onEdit={vi.fn()}
  162. />,
  163. );
  164. expect(await screen.findByText('My Telegram')).toBeInTheDocument();
  165. expect(screen.queryByText('Reactions')).not.toBeInTheDocument();
  166. });
  167. });