ConfirmOutcomeDialog.test.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * Tests for the post-print outcome confirmation dialog (#1898).
  3. */
  4. import { describe, it, expect, beforeEach, vi } from 'vitest';
  5. import { screen, waitFor } from '@testing-library/react';
  6. import userEvent from '@testing-library/user-event';
  7. import { render } from '../utils';
  8. import { ConfirmOutcomeDialog } from '../../components/ConfirmOutcomeDialog';
  9. import { http, HttpResponse } from 'msw';
  10. import { server } from '../mocks/server';
  11. // The reject flow can hand off into the full PrintModal — not under test here.
  12. vi.mock('../../components/PrintModal', () => ({
  13. PrintModal: () => <div data-testid="print-modal" />,
  14. }));
  15. const baseArchive = {
  16. id: 42,
  17. printer_id: 1,
  18. filename: 'bracket.gcode.3mf',
  19. file_path: 'archives/bracket.gcode.3mf',
  20. file_size: 1024,
  21. content_hash: null,
  22. thumbnail_path: null,
  23. timelapse_path: null,
  24. print_name: 'Bracket',
  25. print_time_seconds: 3600,
  26. filament_used_grams: 20,
  27. filament_type: 'PLA',
  28. filament_color: null,
  29. layer_height: 0.2,
  30. total_layers: 100,
  31. nozzle_diameter: 0.4,
  32. bed_temperature: 60,
  33. nozzle_temperature: 220,
  34. status: 'completed',
  35. started_at: null,
  36. completed_at: null,
  37. extra_data: null,
  38. makerworld_url: null,
  39. designer: null,
  40. external_url: null,
  41. is_favorite: false,
  42. tags: null,
  43. notes: null,
  44. cost: null,
  45. photos: ['finish_1.jpg'],
  46. failure_reason: null,
  47. user_verdict: null,
  48. user_verdict_source: null,
  49. confirm_requested: true,
  50. quantity: 1,
  51. energy_kwh: null,
  52. energy_cost: null,
  53. created_at: '2026-09-01T00:00:00Z',
  54. created_by_id: null,
  55. created_by_username: null,
  56. run_count: 1,
  57. last_run_at: null,
  58. total_filament_actual_grams: null,
  59. successful_run_count: 1,
  60. failed_run_count: 0,
  61. };
  62. describe('ConfirmOutcomeDialog', () => {
  63. let patchPayload: Record<string, unknown> | null;
  64. beforeEach(() => {
  65. patchPayload = null;
  66. server.use(
  67. http.get('/api/v1/archives/42', () => HttpResponse.json(baseArchive)),
  68. http.patch('/api/v1/archives/42', async ({ request }) => {
  69. patchPayload = (await request.json()) as Record<string, unknown>;
  70. return HttpResponse.json({ ...baseArchive, ...patchPayload });
  71. }),
  72. http.get('/api/v1/settings/', () => HttpResponse.json({})),
  73. );
  74. });
  75. it('shows the finish photo and records a good verdict', async () => {
  76. const user = userEvent.setup();
  77. const onClose = vi.fn();
  78. render(<ConfirmOutcomeDialog archiveId={42} onClose={onClose} />);
  79. await waitFor(() => {
  80. expect(screen.getByText('Bracket')).toBeInTheDocument();
  81. });
  82. // Finish photo (photos[0]) is rendered
  83. expect(screen.getByAltText('Bracket')).toBeInTheDocument();
  84. await user.click(screen.getByText('Good'));
  85. await waitFor(() => {
  86. expect(patchPayload).toEqual({ user_verdict: 'good', user_verdict_source: 'dialog' });
  87. });
  88. await waitFor(() => {
  89. expect(onClose).toHaveBeenCalled();
  90. });
  91. });
  92. it('records a reject with an optional reason', async () => {
  93. const user = userEvent.setup();
  94. const onClose = vi.fn();
  95. render(<ConfirmOutcomeDialog archiveId={42} onClose={onClose} />);
  96. await waitFor(() => {
  97. expect(screen.getByText('Bracket')).toBeInTheDocument();
  98. });
  99. await user.click(screen.getByText('Reject'));
  100. // Reason step appears
  101. const select = await screen.findByRole('combobox');
  102. await user.selectOptions(select, 'warping');
  103. await user.click(screen.getByText('Save'));
  104. await waitFor(() => {
  105. expect(patchPayload).toEqual({
  106. user_verdict: 'reject',
  107. failure_reason: 'warping',
  108. user_verdict_source: 'dialog',
  109. });
  110. });
  111. await waitFor(() => {
  112. expect(onClose).toHaveBeenCalled();
  113. });
  114. });
  115. it('reject-and-reprint hands off into the print modal after saving', async () => {
  116. const user = userEvent.setup();
  117. render(<ConfirmOutcomeDialog archiveId={42} onClose={vi.fn()} />);
  118. await waitFor(() => {
  119. expect(screen.getByText('Bracket')).toBeInTheDocument();
  120. });
  121. await user.click(screen.getByText('Reject'));
  122. await user.click(await screen.findByText('Print again'));
  123. await waitFor(() => {
  124. expect(patchPayload).toEqual({ user_verdict: 'reject', user_verdict_source: 'dialog' });
  125. });
  126. await waitFor(() => {
  127. expect(screen.getByTestId('print-modal')).toBeInTheDocument();
  128. });
  129. });
  130. it('shows the already-decided state instead of the buttons', async () => {
  131. server.use(
  132. http.get('/api/v1/archives/42', () =>
  133. HttpResponse.json({ ...baseArchive, user_verdict: 'reject' })
  134. ),
  135. );
  136. render(<ConfirmOutcomeDialog archiveId={42} onClose={vi.fn()} />);
  137. await waitFor(() => {
  138. expect(screen.getByText('Already marked as rejected.')).toBeInTheDocument();
  139. });
  140. expect(screen.queryByText('Good')).not.toBeInTheDocument();
  141. // No source on file (an archive answered before #1898 recorded it): no hint.
  142. expect(screen.queryByTestId('verdict-source-hint')).not.toBeInTheDocument();
  143. });
  144. it('explains a verdict the plate-clear default recorded', async () => {
  145. server.use(
  146. http.get('/api/v1/archives/42', () =>
  147. HttpResponse.json({ ...baseArchive, user_verdict: 'good', user_verdict_source: 'plate_clear' })
  148. ),
  149. );
  150. render(<ConfirmOutcomeDialog archiveId={42} onClose={vi.fn()} />);
  151. await waitFor(() => {
  152. expect(screen.getByText('Already confirmed as a good part.')).toBeInTheDocument();
  153. });
  154. expect(screen.getByTestId('verdict-source-hint')).toHaveTextContent(
  155. 'Recorded when the plate was cleared.'
  156. );
  157. });
  158. });