NotificationProviderCardAiFailureDetection.test.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * Tests for the AI Failure Detection toggle on NotificationProviderCard (#1794).
  3. *
  4. * Before #1794, Obico failure detection rode the multiplexed
  5. * on_printer_error toggle so users couldn't subscribe to one without the
  6. * other. These tests pin the standalone toggle:
  7. * - Summary badge renders when enabled.
  8. * - The toggle row appears in the expanded settings panel.
  9. * - Flipping the toggle PATCHes the correct field.
  10. */
  11. import { describe, it, expect, afterEach, vi } from 'vitest';
  12. import { screen, waitFor, within } from '@testing-library/react';
  13. import userEvent from '@testing-library/user-event';
  14. import { http, HttpResponse } from 'msw';
  15. import { render } from '../utils';
  16. import { server } from '../mocks/server';
  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: 'Test Provider',
  27. provider_type: 'ntfy',
  28. enabled: true,
  29. config: { server: 'https://ntfy.sh', topic: 'bambuddy' },
  30. on_print_start: false,
  31. on_print_complete: false,
  32. on_print_failed: false,
  33. on_print_stopped: false,
  34. on_print_progress: false,
  35. on_print_missing_spool_assignment: false,
  36. on_printer_offline: false,
  37. on_printer_error: false,
  38. on_ai_failure_detection: false,
  39. on_filament_low: false,
  40. on_maintenance_due: false,
  41. on_ams_humidity_high: false,
  42. on_ams_temperature_high: false,
  43. on_ams_ht_humidity_high: false,
  44. on_ams_ht_temperature_high: false,
  45. on_plate_not_empty: false,
  46. on_bed_cooled: false,
  47. on_first_layer_complete: false,
  48. on_queue_job_added: false,
  49. on_queue_job_assigned: false,
  50. on_queue_job_started: false,
  51. on_queue_job_waiting: false,
  52. on_queue_job_skipped: false,
  53. on_queue_job_failed: false,
  54. on_queue_completed: false,
  55. on_stock_reorder_alert: false,
  56. on_stock_break_alert: false,
  57. quiet_hours_enabled: false,
  58. quiet_hours_start: null,
  59. quiet_hours_end: null,
  60. daily_digest_enabled: false,
  61. daily_digest_time: null,
  62. printer_id: null,
  63. last_success: null,
  64. last_error: null,
  65. last_error_at: null,
  66. created_at: '2026-06-22T00:00:00Z',
  67. updated_at: '2026-06-22T00:00:00Z',
  68. ...overrides,
  69. };
  70. }
  71. describe('NotificationProviderCard — AI Failure Detection badge', () => {
  72. it('renders the badge when on_ai_failure_detection is true', async () => {
  73. render(
  74. <NotificationProviderCard
  75. provider={buildProvider({ on_ai_failure_detection: true })}
  76. onEdit={vi.fn()}
  77. />,
  78. );
  79. expect(await screen.findByText('AI Failure Detection')).toBeInTheDocument();
  80. });
  81. it('omits the badge when on_ai_failure_detection is false', async () => {
  82. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  83. await screen.findByText('Test Provider');
  84. expect(screen.queryByText('AI Failure Detection')).not.toBeInTheDocument();
  85. });
  86. });
  87. describe('NotificationProviderCard — AI Failure Detection toggle', () => {
  88. it('renders the toggle in the expanded settings panel', async () => {
  89. const user = userEvent.setup();
  90. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  91. await user.click(await screen.findByText(/event settings/i));
  92. expect(await screen.findByText('AI Failure Detection')).toBeInTheDocument();
  93. });
  94. it('PATCHes on_ai_failure_detection (NOT on_printer_error) when toggled on — #1794 regression guard', async () => {
  95. let captured: Record<string, unknown> | null = null;
  96. server.use(
  97. http.patch('*/api/v1/notifications/1', async ({ request }) => {
  98. captured = (await request.json()) as Record<string, unknown>;
  99. return HttpResponse.json(buildProvider({ on_ai_failure_detection: true }));
  100. }),
  101. );
  102. const user = userEvent.setup();
  103. render(<NotificationProviderCard provider={buildProvider()} onEdit={vi.fn()} />);
  104. await user.click(await screen.findByText(/event settings/i));
  105. // The toggle label "AI Failure Detection" is unique to this row.
  106. const label = await screen.findByText('AI Failure Detection');
  107. const row = label.closest('div.flex')!;
  108. const toggle = within(row).getByRole('switch');
  109. await user.click(toggle);
  110. await waitFor(() => expect(captured).not.toBeNull());
  111. expect(captured).toMatchObject({ on_ai_failure_detection: true });
  112. // Critical: must NOT also flip the legacy multiplexed field.
  113. expect(captured).not.toHaveProperty('on_printer_error');
  114. });
  115. });