PrintModalBilling.test.tsx 6.6 KB

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