NotificationProviderCardStockAlerts.test.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. /**
  2. * Tests for stock alert toggles added to NotificationProviderCard.
  3. *
  4. * Coverage:
  5. * - Reorder Alert and Stock Break Alert badges render in the summary strip when enabled.
  6. * - Badges are absent when both flags are false.
  7. * - Inventory Alerts section renders in the expanded settings panel.
  8. * - Toggling a stock alert fires an update mutation with the correct field.
  9. */
  10. import { describe, it, expect, afterEach, vi } from 'vitest';
  11. import { screen, waitFor, within } 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 { NotificationProviderCard } from '../../components/NotificationProviderCard';
  17. import type { NotificationProvider } from '../../api/client';
  18. afterEach(() => {
  19. server.resetHandlers();
  20. vi.restoreAllMocks();
  21. });
  22. function buildProvider(overrides: Partial<NotificationProvider> = {}): NotificationProvider {
  23. return {
  24. id: 1,
  25. name: 'Test Provider',
  26. provider_type: 'ntfy',
  27. enabled: true,
  28. config: { server: 'https://ntfy.sh', topic: 'bambuddy' },
  29. on_print_start: false,
  30. on_print_complete: false,
  31. on_print_failed: false,
  32. on_print_stopped: false,
  33. on_print_progress: false,
  34. on_print_missing_spool_assignment: false,
  35. on_printer_offline: false,
  36. on_printer_error: false,
  37. on_ai_failure_detection: false,
  38. on_filament_low: false,
  39. on_maintenance_due: false,
  40. on_ams_humidity_high: false,
  41. on_ams_temperature_high: false,
  42. on_ams_ht_humidity_high: false,
  43. on_ams_ht_temperature_high: false,
  44. on_plate_not_empty: false,
  45. on_bed_cooled: false,
  46. on_first_layer_complete: false,
  47. on_queue_job_added: false,
  48. on_queue_job_assigned: false,
  49. on_queue_job_started: false,
  50. on_queue_job_waiting: false,
  51. on_queue_job_skipped: false,
  52. on_queue_job_failed: false,
  53. on_queue_completed: false,
  54. on_stock_reorder_alert: false,
  55. on_stock_break_alert: false,
  56. quiet_hours_enabled: false,
  57. quiet_hours_start: null,
  58. quiet_hours_end: null,
  59. daily_digest_enabled: false,
  60. daily_digest_time: null,
  61. printer_id: null,
  62. last_success: null,
  63. last_error: null,
  64. last_error_at: null,
  65. created_at: '2026-04-25T00:00:00Z',
  66. updated_at: '2026-04-25T00:00:00Z',
  67. ...overrides,
  68. };
  69. }
  70. describe('NotificationProviderCard — stock alert badges', () => {
  71. it('shows Reorder Alert badge when on_stock_reorder_alert is true', async () => {
  72. render(<NotificationProviderCard provider={buildProvider({ on_stock_reorder_alert: true })} onEdit={vi.fn()} />);
  73. expect(await screen.findByText('Reorder Alert')).toBeInTheDocument();
  74. });
  75. it('shows Stock Break Alert badge when on_stock_break_alert is true', async () => {
  76. render(<NotificationProviderCard provider={buildProvider({ on_stock_break_alert: true })} onEdit={vi.fn()} />);
  77. expect(await screen.findByText('Stock Break Alert')).toBeInTheDocument();
  78. });
  79. it('shows both badges when both flags are true', async () => {
  80. render(
  81. <NotificationProviderCard
  82. provider={buildProvider({ on_stock_reorder_alert: true, on_stock_break_alert: true })}
  83. onEdit={vi.fn()}
  84. />,
  85. );
  86. expect(await screen.findByText('Reorder Alert')).toBeInTheDocument();
  87. expect(screen.getByText('Stock Break Alert')).toBeInTheDocument();
  88. });
  89. it('shows no stock alert badges when both flags are false', async () => {
  90. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  91. // Wait for the card to mount
  92. await screen.findByText('Test Provider');
  93. expect(screen.queryByText('Reorder Alert')).not.toBeInTheDocument();
  94. expect(screen.queryByText('Stock Break Alert')).not.toBeInTheDocument();
  95. });
  96. });
  97. describe('NotificationProviderCard — Inventory Alerts expanded section', () => {
  98. it('renders the Inventory Alerts section header when settings are expanded', async () => {
  99. const user = userEvent.setup();
  100. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  101. const settingsBtn = await screen.findByText(/event settings/i);
  102. await user.click(settingsBtn);
  103. expect(await screen.findByText('Inventory Alerts')).toBeInTheDocument();
  104. });
  105. it('renders both stock alert toggles in the expanded section', async () => {
  106. const user = userEvent.setup();
  107. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  108. await user.click(await screen.findByText(/event settings/i));
  109. const section = (await screen.findByText('Inventory Alerts')).closest('div')!;
  110. expect(within(section).getByText('Reorder Alert')).toBeInTheDocument();
  111. expect(within(section).getByText('Stock Break Alert')).toBeInTheDocument();
  112. });
  113. it('stock alert toggles reflect the provider state', async () => {
  114. const user = userEvent.setup();
  115. render(
  116. <NotificationProviderCard
  117. provider={buildProvider({ on_stock_reorder_alert: true, on_stock_break_alert: false })}
  118. onEdit={vi.fn()}
  119. />,
  120. );
  121. await user.click(await screen.findByText(/event settings/i));
  122. const section = (await screen.findByText('Inventory Alerts')).closest('div')!;
  123. const switches = within(section).getAllByRole('switch');
  124. // First switch = reorder alert (true), second = break alert (false)
  125. expect(switches[0]).toHaveAttribute('aria-checked', 'true');
  126. expect(switches[1]).toHaveAttribute('aria-checked', 'false');
  127. });
  128. it('toggling Reorder Alert sends correct PATCH payload', async () => {
  129. let captured: unknown = null;
  130. server.use(
  131. http.patch('*/api/v1/notifications/1', async ({ request }) => {
  132. captured = await request.json();
  133. return HttpResponse.json(buildProvider({ on_stock_reorder_alert: true }));
  134. }),
  135. );
  136. const user = userEvent.setup();
  137. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  138. await user.click(await screen.findByText(/event settings/i));
  139. const section = (await screen.findByText('Inventory Alerts')).closest('div')!;
  140. const [reorderSwitch] = within(section).getAllByRole('switch');
  141. await user.click(reorderSwitch);
  142. await waitFor(() => expect(captured).not.toBeNull());
  143. expect(captured).toMatchObject({ on_stock_reorder_alert: true });
  144. });
  145. it('toggling Stock Break Alert sends correct PATCH payload', async () => {
  146. let captured: unknown = null;
  147. server.use(
  148. http.patch('*/api/v1/notifications/1', async ({ request }) => {
  149. captured = await request.json();
  150. return HttpResponse.json(buildProvider({ on_stock_break_alert: true }));
  151. }),
  152. );
  153. const user = userEvent.setup();
  154. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  155. await user.click(await screen.findByText(/event settings/i));
  156. const section = (await screen.findByText('Inventory Alerts')).closest('div')!;
  157. const switches = within(section).getAllByRole('switch');
  158. await user.click(switches[1]);
  159. await waitFor(() => expect(captured).not.toBeNull());
  160. expect(captured).toMatchObject({ on_stock_break_alert: true });
  161. });
  162. });