VirtualPrinterSettings.test.tsx 17 KB

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