PrintModalBilling.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * Tests for billing-related PrintModal request payloads.
  3. */
  4. import React from 'react';
  5. import { BrowserRouter } from 'react-router-dom';
  6. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  7. import { render, screen, waitFor } from '@testing-library/react';
  8. import userEvent from '@testing-library/user-event';
  9. import { http, HttpResponse } from 'msw';
  10. import { describe, it, expect, vi, beforeEach } from 'vitest';
  11. import { server } from '../mocks/server';
  12. import { ThemeProvider } from '../../contexts/ThemeContext';
  13. const mockShowToast = vi.fn();
  14. const mockUseAuth = {
  15. user: { id: 1, username: 'finance-user', permissions: ['cost_centers:read_own', 'printers:control'] },
  16. authEnabled: true,
  17. loading: false,
  18. isAdmin: false,
  19. login: vi.fn(),
  20. loginWithToken: vi.fn(),
  21. logout: vi.fn(),
  22. refreshAuth: vi.fn(),
  23. hasPermission: vi.fn((permission: string) => permission === 'cost_centers:read_own' || permission === 'printers:control'),
  24. hasAnyPermission: vi.fn(() => true),
  25. canModify: vi.fn(() => true),
  26. };
  27. vi.mock('../../contexts/AuthContext', async (importOriginal) => {
  28. const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
  29. return {
  30. ...actual,
  31. useAuth: () => mockUseAuth,
  32. };
  33. });
  34. vi.mock('../../contexts/ToastContext', async (importOriginal) => {
  35. const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
  36. return {
  37. ...actual,
  38. useToast: () => ({ showToast: mockShowToast }),
  39. };
  40. });
  41. import { PrintModal } from '../../components/PrintModal';
  42. function createTestQueryClient() {
  43. return new QueryClient({
  44. defaultOptions: {
  45. queries: { retry: false, gcTime: 0 },
  46. mutations: { retry: false },
  47. },
  48. });
  49. }
  50. function renderWithProviders(ui: React.ReactElement) {
  51. const queryClient = createTestQueryClient();
  52. return render(
  53. <QueryClientProvider client={queryClient}>
  54. <BrowserRouter>
  55. <ThemeProvider>{ui}</ThemeProvider>
  56. </BrowserRouter>
  57. </QueryClientProvider>
  58. );
  59. }
  60. const mockPrinters = [
  61. { id: 1, name: 'X1 Carbon', model: 'X1C', ip_address: '192.168.1.100', enabled: true, is_active: true },
  62. ];
  63. const mockQueueItem = {
  64. id: 9,
  65. printer_id: 1,
  66. archive_id: 1,
  67. position: 1,
  68. scheduled_time: null,
  69. require_previous_success: false,
  70. auto_off_after: false,
  71. gcode_injection: false,
  72. manual_start: false,
  73. ams_mapping: null,
  74. plate_id: null,
  75. bed_levelling: true,
  76. flow_cali: false,
  77. vibration_cali: true,
  78. layer_inspect: false,
  79. timelapse: false,
  80. use_ams: true,
  81. status: 'pending',
  82. started_at: null,
  83. completed_at: null,
  84. error_message: null,
  85. created_at: '2024-01-01T00:00:00Z',
  86. archive_name: 'Billing Print',
  87. archive_thumbnail: null,
  88. printer_name: 'X1 Carbon',
  89. print_time_seconds: 3600,
  90. batch_id: null,
  91. batch_name: null,
  92. cost_center_id: 42,
  93. estimated_cost: 12.34,
  94. };
  95. describe('PrintModal billing payloads', () => {
  96. beforeEach(() => {
  97. vi.clearAllMocks();
  98. server.use(
  99. // This user may not read /settings: what the dialog needs comes from
  100. // /settings/ui-flags.
  101. http.get('/api/v1/settings/ui-flags', () => {
  102. return HttpResponse.json({
  103. currency: 'USD',
  104. default_filament_cost: 25,
  105. billing_enabled: true,
  106. default_bed_levelling: true,
  107. default_flow_cali: false,
  108. default_vibration_cali: true,
  109. default_layer_inspect: false,
  110. default_timelapse: false,
  111. stagger_group_size: 2,
  112. stagger_interval_minutes: 5,
  113. per_printer_mapping_expanded: false,
  114. date_format: 'system',
  115. time_format: 'system',
  116. });
  117. }),
  118. http.get('/api/v1/printers/', () => HttpResponse.json(mockPrinters)),
  119. http.get('/api/v1/printers/:id/status', () => {
  120. return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] });
  121. }),
  122. http.get('/api/v1/archives/:id', () => {
  123. return HttpResponse.json({ id: 1, sliced_for_model: null });
  124. }),
  125. http.get('/api/v1/archives/:id/plates', () => {
  126. return HttpResponse.json({ is_multi_plate: false, plates: [] });
  127. }),
  128. http.get('/api/v1/archives/:id/filament-requirements', () => {
  129. return HttpResponse.json({ filaments: [] });
  130. }),
  131. http.get('/api/v1/finance/cost-centers/mine', () => {
  132. return HttpResponse.json([
  133. {
  134. id: 42,
  135. name: 'Lab',
  136. is_private: false,
  137. owner_user_id: null,
  138. is_active: true,
  139. total_balance: 0,
  140. total_budget: 100,
  141. monthly_budget: 100,
  142. budget_mode: 'monthly',
  143. budget_limit: 100,
  144. budget_used: 0,
  145. budget_available: 88,
  146. can_print: true,
  147. },
  148. ]);
  149. }),
  150. http.patch('/api/v1/queue/:id', async ({ request }) => {
  151. const body = await request.json() as Record<string, unknown>;
  152. expect(body.cost_center_id).toBe(42);
  153. expect(body.estimated_cost).toBe(12.34);
  154. return HttpResponse.json({ id: 9, status: 'pending' });
  155. })
  156. );
  157. });
  158. it('includes the selected cost center and estimate when saving an edit-queue item', async () => {
  159. const user = userEvent.setup();
  160. renderWithProviders(
  161. <PrintModal
  162. mode="edit-queue-item"
  163. archiveId={1}
  164. archiveName="Billing Print"
  165. queueItem={mockQueueItem as never}
  166. onClose={vi.fn()}
  167. onSuccess={vi.fn()}
  168. />
  169. );
  170. await waitFor(() => {
  171. expect(screen.getByText('Lab')).not.toBeNull();
  172. });
  173. await user.click(screen.getByRole('button', { name: /save/i }));
  174. await waitFor(() => {
  175. expect(mockShowToast).toHaveBeenCalled();
  176. });
  177. });
  178. it('labels a cost center without a budget as unlimited', async () => {
  179. server.use(
  180. http.get('/api/v1/finance/cost-centers/mine', () => {
  181. return HttpResponse.json([
  182. {
  183. id: 42,
  184. name: 'Unlimited Lab',
  185. is_private: false,
  186. owner_user_id: null,
  187. is_active: true,
  188. total_balance: -25,
  189. total_budget: null,
  190. monthly_budget: null,
  191. budget_mode: 'none',
  192. budget_limit: null,
  193. budget_used: null,
  194. budget_available: null,
  195. can_print: true,
  196. },
  197. ]);
  198. }),
  199. );
  200. renderWithProviders(
  201. <PrintModal
  202. mode="edit-queue-item"
  203. archiveId={1}
  204. archiveName="Billing Print"
  205. queueItem={mockQueueItem as never}
  206. onClose={vi.fn()}
  207. onSuccess={vi.fn()}
  208. />,
  209. );
  210. expect(await screen.findByText('Unlimited – no budget limit is set.')).not.toBeNull();
  211. });
  212. });