VirtualPrinterSettings.test.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /**
  2. * Tests for the VirtualPrinterSettings component.
  3. *
  4. * Tests the virtual printer configuration UI including:
  5. * - Enable/disable toggle
  6. * - Access code management
  7. * - Archive mode selection
  8. * - Status display
  9. */
  10. import { describe, it, expect, vi, beforeEach } from 'vitest';
  11. import { screen, waitFor } from '@testing-library/react';
  12. import userEvent from '@testing-library/user-event';
  13. import { render } from '../utils';
  14. import { VirtualPrinterSettings } from '../../components/VirtualPrinterSettings';
  15. // Mock the API client
  16. vi.mock('../../api/client', () => ({
  17. api: {
  18. getSettings: vi.fn().mockResolvedValue({}),
  19. updateSettings: vi.fn().mockResolvedValue({}),
  20. },
  21. virtualPrinterApi: {
  22. getSettings: vi.fn(),
  23. updateSettings: vi.fn(),
  24. getModels: vi.fn().mockResolvedValue({
  25. models: {
  26. '3DPrinter-X1-Carbon': 'X1C',
  27. 'C12': 'P1S',
  28. 'N7': 'P2S',
  29. },
  30. }),
  31. },
  32. }));
  33. // Import mocked module
  34. import { virtualPrinterApi } from '../../api/client';
  35. // Mock data factory
  36. const createMockSettings = (overrides = {}) => ({
  37. enabled: false,
  38. access_code_set: false,
  39. mode: 'immediate' as const,
  40. model: '3DPrinter-X1-Carbon',
  41. target_printer_id: null as number | null,
  42. remote_interface_ip: null as string | null,
  43. status: {
  44. enabled: false,
  45. running: false,
  46. mode: 'immediate',
  47. name: 'Bambuddy',
  48. serial: '00M00A391800001',
  49. model: '3DPrinter-X1-Carbon',
  50. model_name: 'X1C',
  51. pending_files: 0,
  52. },
  53. ...overrides,
  54. });
  55. describe('VirtualPrinterSettings', () => {
  56. beforeEach(() => {
  57. vi.clearAllMocks();
  58. // Default mock implementation
  59. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(createMockSettings());
  60. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(createMockSettings());
  61. });
  62. describe('rendering', () => {
  63. it('renders loading state initially', () => {
  64. // Delay the API response to catch loading state
  65. vi.mocked(virtualPrinterApi.getSettings).mockImplementation(
  66. () => new Promise(() => {}) // Never resolves
  67. );
  68. render(<VirtualPrinterSettings />);
  69. // Should show loading spinner
  70. expect(document.querySelector('.animate-spin')).toBeInTheDocument();
  71. });
  72. it('renders component title', async () => {
  73. render(<VirtualPrinterSettings />);
  74. await waitFor(() => {
  75. expect(screen.getByText('Virtual Printer')).toBeInTheDocument();
  76. });
  77. });
  78. it('renders enable toggle', async () => {
  79. render(<VirtualPrinterSettings />);
  80. await waitFor(() => {
  81. expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
  82. });
  83. });
  84. it('renders access code section', async () => {
  85. render(<VirtualPrinterSettings />);
  86. await waitFor(() => {
  87. expect(screen.getByText('Access Code')).toBeInTheDocument();
  88. });
  89. });
  90. it('renders mode section', async () => {
  91. render(<VirtualPrinterSettings />);
  92. await waitFor(() => {
  93. expect(screen.getByText('Mode')).toBeInTheDocument();
  94. });
  95. });
  96. it('renders how it works info', async () => {
  97. render(<VirtualPrinterSettings />);
  98. await waitFor(() => {
  99. expect(screen.getByText('How it works:')).toBeInTheDocument();
  100. });
  101. });
  102. });
  103. describe('status indicator', () => {
  104. it('shows Stopped when not running', async () => {
  105. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  106. createMockSettings({ status: { ...createMockSettings().status, running: false } })
  107. );
  108. render(<VirtualPrinterSettings />);
  109. await waitFor(() => {
  110. expect(screen.getByText('Stopped')).toBeInTheDocument();
  111. });
  112. });
  113. it('shows Running when active', async () => {
  114. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  115. createMockSettings({
  116. enabled: true,
  117. status: { ...createMockSettings().status, enabled: true, running: true },
  118. })
  119. );
  120. render(<VirtualPrinterSettings />);
  121. await waitFor(() => {
  122. expect(screen.getByText('Running')).toBeInTheDocument();
  123. });
  124. });
  125. it('shows status details when running', async () => {
  126. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  127. createMockSettings({
  128. enabled: true,
  129. status: {
  130. enabled: true,
  131. running: true,
  132. mode: 'immediate',
  133. name: 'Bambuddy',
  134. serial: '00M00A391800001',
  135. model: '3DPrinter-X1-Carbon',
  136. model_name: 'X1C',
  137. pending_files: 0,
  138. },
  139. })
  140. );
  141. render(<VirtualPrinterSettings />);
  142. await waitFor(() => {
  143. expect(screen.getByText('Status Details')).toBeInTheDocument();
  144. expect(screen.getByText('Bambuddy')).toBeInTheDocument();
  145. expect(screen.getByText('00M00A391800001')).toBeInTheDocument();
  146. });
  147. });
  148. });
  149. describe('access code', () => {
  150. it('shows warning when access code not set', async () => {
  151. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  152. createMockSettings({ access_code_set: false })
  153. );
  154. render(<VirtualPrinterSettings />);
  155. await waitFor(() => {
  156. expect(screen.getByText('No access code set - required to enable')).toBeInTheDocument();
  157. });
  158. });
  159. it('shows success when access code is set', async () => {
  160. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  161. createMockSettings({ access_code_set: true })
  162. );
  163. render(<VirtualPrinterSettings />);
  164. await waitFor(() => {
  165. expect(screen.getByText('Access code is set')).toBeInTheDocument();
  166. });
  167. });
  168. it('shows character count while typing', async () => {
  169. const user = userEvent.setup();
  170. render(<VirtualPrinterSettings />);
  171. await waitFor(() => {
  172. expect(screen.getByText('Access Code')).toBeInTheDocument();
  173. });
  174. const input = screen.getByPlaceholderText('Enter 8-char code');
  175. await user.type(input, '1234');
  176. expect(screen.getByText('(4/8)')).toBeInTheDocument();
  177. });
  178. it('saves access code on button click', async () => {
  179. const user = userEvent.setup();
  180. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  181. createMockSettings({ access_code_set: true })
  182. );
  183. render(<VirtualPrinterSettings />);
  184. await waitFor(() => {
  185. expect(screen.getByText('Access Code')).toBeInTheDocument();
  186. });
  187. const input = screen.getByPlaceholderText('Enter 8-char code');
  188. await user.type(input, '12345678');
  189. const saveButton = screen.getByRole('button', { name: 'Save' });
  190. await user.click(saveButton);
  191. await waitFor(() => {
  192. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({
  193. access_code: '12345678',
  194. });
  195. });
  196. });
  197. it('toggles password visibility', async () => {
  198. const user = userEvent.setup();
  199. render(<VirtualPrinterSettings />);
  200. await waitFor(() => {
  201. expect(screen.getByText('Access Code')).toBeInTheDocument();
  202. });
  203. const input = screen.getByPlaceholderText('Enter 8-char code');
  204. expect(input).toHaveAttribute('type', 'password');
  205. // Find and click the visibility toggle (eye icon button)
  206. const toggleButtons = screen.getAllByRole('button');
  207. const visibilityToggle = toggleButtons.find(
  208. (btn) => btn.querySelector('svg') && btn.className.includes('absolute')
  209. );
  210. if (visibilityToggle) {
  211. await user.click(visibilityToggle);
  212. expect(input).toHaveAttribute('type', 'text');
  213. }
  214. });
  215. });
  216. describe('mode selection', () => {
  217. it('renders Archive mode option', async () => {
  218. render(<VirtualPrinterSettings />);
  219. await waitFor(() => {
  220. expect(screen.getByText('Archive')).toBeInTheDocument();
  221. expect(screen.getByText('Archive files immediately')).toBeInTheDocument();
  222. });
  223. });
  224. it('renders Review mode option', async () => {
  225. render(<VirtualPrinterSettings />);
  226. await waitFor(() => {
  227. expect(screen.getByText('Review')).toBeInTheDocument();
  228. expect(screen.getByText('Review before archiving')).toBeInTheDocument();
  229. });
  230. });
  231. it('renders Queue mode option', async () => {
  232. render(<VirtualPrinterSettings />);
  233. await waitFor(() => {
  234. expect(screen.getByText('Queue')).toBeInTheDocument();
  235. expect(screen.getByText('Archive and add to queue')).toBeInTheDocument();
  236. });
  237. });
  238. it('highlights current mode (review)', async () => {
  239. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  240. createMockSettings({ mode: 'review' })
  241. );
  242. render(<VirtualPrinterSettings />);
  243. await waitFor(() => {
  244. const reviewButton = screen.getByText('Review').closest('button');
  245. expect(reviewButton?.className).toContain('border-bambu-green');
  246. });
  247. });
  248. it('highlights current mode (legacy queue maps to review)', async () => {
  249. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  250. createMockSettings({ mode: 'queue' })
  251. );
  252. render(<VirtualPrinterSettings />);
  253. await waitFor(() => {
  254. const reviewButton = screen.getByText('Review').closest('button');
  255. expect(reviewButton?.className).toContain('border-bambu-green');
  256. });
  257. });
  258. it('changes mode to review on click', async () => {
  259. const user = userEvent.setup();
  260. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  261. createMockSettings({ mode: 'review' })
  262. );
  263. render(<VirtualPrinterSettings />);
  264. await waitFor(() => {
  265. expect(screen.getByText('Review')).toBeInTheDocument();
  266. });
  267. const reviewButton = screen.getByText('Review').closest('button');
  268. if (reviewButton) {
  269. await user.click(reviewButton);
  270. }
  271. await waitFor(() => {
  272. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'review' });
  273. });
  274. });
  275. it('changes mode to print_queue on click', async () => {
  276. const user = userEvent.setup();
  277. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  278. createMockSettings({ mode: 'print_queue' })
  279. );
  280. render(<VirtualPrinterSettings />);
  281. await waitFor(() => {
  282. expect(screen.getByText('Queue')).toBeInTheDocument();
  283. });
  284. const queueButton = screen.getByText('Queue').closest('button');
  285. if (queueButton) {
  286. await user.click(queueButton);
  287. }
  288. await waitFor(() => {
  289. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'print_queue' });
  290. });
  291. });
  292. });
  293. describe('enable/disable toggle', () => {
  294. it('cannot enable without access code', async () => {
  295. const user = userEvent.setup();
  296. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  297. createMockSettings({ enabled: false, access_code_set: false })
  298. );
  299. render(<VirtualPrinterSettings />);
  300. await waitFor(() => {
  301. expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
  302. });
  303. // Find the toggle switch (it's a button with relative class containing the slider)
  304. const allButtons = screen.getAllByRole('button');
  305. const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
  306. if (toggle) {
  307. await user.click(toggle);
  308. }
  309. // Should not call update API (no access code set)
  310. expect(virtualPrinterApi.updateSettings).not.toHaveBeenCalled();
  311. });
  312. it('can enable when access code is set', async () => {
  313. const user = userEvent.setup();
  314. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  315. createMockSettings({ enabled: false, access_code_set: true })
  316. );
  317. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  318. createMockSettings({ enabled: true, access_code_set: true })
  319. );
  320. render(<VirtualPrinterSettings />);
  321. await waitFor(() => {
  322. expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
  323. });
  324. // Find the toggle switch (it's a button with rounded-full and w-12 classes)
  325. const allButtons = screen.getAllByRole('button');
  326. const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
  327. expect(toggle).toBeDefined();
  328. if (toggle) {
  329. await user.click(toggle);
  330. }
  331. await waitFor(() => {
  332. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith(
  333. expect.objectContaining({ enabled: true })
  334. );
  335. });
  336. });
  337. it('can disable when enabled', async () => {
  338. const user = userEvent.setup();
  339. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  340. createMockSettings({ enabled: true, access_code_set: true })
  341. );
  342. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  343. createMockSettings({ enabled: false, access_code_set: true })
  344. );
  345. render(<VirtualPrinterSettings />);
  346. await waitFor(() => {
  347. expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument();
  348. });
  349. // Find the toggle switch
  350. const allButtons = screen.getAllByRole('button');
  351. const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12'));
  352. expect(toggle).toBeDefined();
  353. if (toggle) {
  354. await user.click(toggle);
  355. }
  356. await waitFor(() => {
  357. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith(
  358. expect.objectContaining({ enabled: false })
  359. );
  360. });
  361. });
  362. });
  363. describe('info section', () => {
  364. it('shows setup required warning', async () => {
  365. render(<VirtualPrinterSettings />);
  366. await waitFor(() => {
  367. expect(screen.getByText('Setup Required')).toBeInTheDocument();
  368. });
  369. });
  370. it('shows link to setup guide', async () => {
  371. render(<VirtualPrinterSettings />);
  372. await waitFor(() => {
  373. expect(screen.getByText('Read the setup guide before enabling')).toBeInTheDocument();
  374. });
  375. });
  376. it('shows how it works section', async () => {
  377. render(<VirtualPrinterSettings />);
  378. await waitFor(() => {
  379. expect(screen.getByText('How it works:')).toBeInTheDocument();
  380. expect(screen.getByText(/Complete the setup guide for your platform/)).toBeInTheDocument();
  381. });
  382. });
  383. });
  384. describe('proxy mode', () => {
  385. it('renders Proxy mode option', async () => {
  386. render(<VirtualPrinterSettings />);
  387. await waitFor(() => {
  388. expect(screen.getByText('Proxy')).toBeInTheDocument();
  389. expect(screen.getByText('Relay to real printer')).toBeInTheDocument();
  390. });
  391. });
  392. it('highlights proxy mode when selected', async () => {
  393. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  394. createMockSettings({ mode: 'proxy', target_printer_id: 1 })
  395. );
  396. render(<VirtualPrinterSettings />);
  397. await waitFor(() => {
  398. const proxyButton = screen.getByText('Proxy').closest('button');
  399. expect(proxyButton?.className).toContain('border-blue-500');
  400. });
  401. });
  402. it('shows proxy status details when running in proxy mode', async () => {
  403. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  404. createMockSettings({
  405. enabled: true,
  406. mode: 'proxy',
  407. target_printer_id: 1,
  408. status: {
  409. enabled: true,
  410. running: true,
  411. mode: 'proxy',
  412. name: 'Bambuddy (Proxy)',
  413. serial: '00M00A391800001',
  414. model: '3DPrinter-X1-Carbon',
  415. model_name: 'X1C',
  416. pending_files: 0,
  417. proxy: {
  418. running: true,
  419. target_host: '192.168.1.100',
  420. ftp_port: 990, // Privileged port for Bambu Studio compatibility
  421. mqtt_port: 8883,
  422. ftp_connections: 1,
  423. mqtt_connections: 2,
  424. },
  425. },
  426. })
  427. );
  428. render(<VirtualPrinterSettings />);
  429. await waitFor(() => {
  430. expect(screen.getByText('Running')).toBeInTheDocument();
  431. expect(screen.getByText('Status Details')).toBeInTheDocument();
  432. // IP address appears in multiple places (target printer and status details)
  433. expect(screen.getAllByText('192.168.1.100').length).toBeGreaterThan(0);
  434. });
  435. });
  436. it('shows target printer dropdown in proxy mode', async () => {
  437. vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(
  438. createMockSettings({ mode: 'proxy' })
  439. );
  440. render(<VirtualPrinterSettings />);
  441. await waitFor(() => {
  442. expect(screen.getByText('Target Printer')).toBeInTheDocument();
  443. expect(screen.getByText('Select a printer...')).toBeInTheDocument();
  444. });
  445. });
  446. it('changes mode to proxy on click', async () => {
  447. const user = userEvent.setup();
  448. vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(
  449. createMockSettings({ mode: 'proxy' })
  450. );
  451. render(<VirtualPrinterSettings />);
  452. await waitFor(() => {
  453. expect(screen.getByText('Proxy')).toBeInTheDocument();
  454. });
  455. const proxyButton = screen.getByText('Proxy').closest('button');
  456. if (proxyButton) {
  457. await user.click(proxyButton);
  458. }
  459. await waitFor(() => {
  460. expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'proxy' });
  461. });
  462. });
  463. });
  464. });