Layout.test.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. /**
  2. * Tests for the Layout component.
  3. */
  4. import { describe, it, expect, beforeEach } from 'vitest';
  5. import { waitFor } from '@testing-library/react';
  6. import { render } from '../utils';
  7. import { Layout } from '../../components/Layout';
  8. import { http, HttpResponse } from 'msw';
  9. import { server } from '../mocks/server';
  10. describe('Layout', () => {
  11. beforeEach(() => {
  12. server.use(
  13. http.get('/api/v1/printers/', () => {
  14. return HttpResponse.json([
  15. { id: 1, name: 'X1 Carbon', model: 'X1C', enabled: true },
  16. ]);
  17. }),
  18. http.get('/api/v1/printers/:id/status', () => {
  19. return HttpResponse.json({
  20. connected: true,
  21. state: 'IDLE',
  22. });
  23. }),
  24. http.get('/api/v1/version', () => {
  25. return HttpResponse.json({ version: '0.1.6', build: 'test' });
  26. }),
  27. http.get('/api/v1/settings/', () => {
  28. return HttpResponse.json({
  29. check_updates: false,
  30. check_printer_firmware: false,
  31. auto_archive: true,
  32. });
  33. }),
  34. http.get('/api/v1/external-links/', () => {
  35. return HttpResponse.json([]);
  36. }),
  37. http.get('/api/v1/smart-plugs/', () => {
  38. return HttpResponse.json([]);
  39. }),
  40. http.get('/api/v1/support/debug-logging', () => {
  41. return HttpResponse.json({ enabled: false });
  42. }),
  43. http.get('/api/v1/queue/', () => {
  44. return HttpResponse.json([]);
  45. }),
  46. http.get('/api/v1/pending-uploads/count', () => {
  47. return HttpResponse.json({ count: 0 });
  48. }),
  49. http.get('/api/v1/updates/check', () => {
  50. return HttpResponse.json({ update_available: false });
  51. }),
  52. http.get('/api/v1/auth/status', () => {
  53. return HttpResponse.json({ auth_enabled: false, requires_setup: false });
  54. }),
  55. http.get('/api/v1/printers/developer-mode-warnings', () => {
  56. return HttpResponse.json([]);
  57. })
  58. );
  59. });
  60. describe('rendering', () => {
  61. it('renders the sidebar', async () => {
  62. render(<Layout />);
  63. // Layout renders as a flex container with sidebar
  64. await waitFor(() => {
  65. const sidebar = document.querySelector('aside');
  66. expect(sidebar).toBeInTheDocument();
  67. });
  68. });
  69. it('renders navigation links', async () => {
  70. render(<Layout />);
  71. await waitFor(() => {
  72. // Navigation links should be present
  73. const links = document.querySelectorAll('a');
  74. expect(links.length).toBeGreaterThan(0);
  75. });
  76. });
  77. });
  78. describe('navigation', () => {
  79. it('has navigation items', async () => {
  80. render(<Layout />);
  81. await waitFor(() => {
  82. // Should have multiple navigation links
  83. const navLinks = document.querySelectorAll('a[href]');
  84. expect(navLinks.length).toBeGreaterThan(0);
  85. });
  86. });
  87. it('includes settings link', async () => {
  88. render(<Layout />);
  89. await waitFor(() => {
  90. // Settings link should exist (route /settings)
  91. const settingsLink = document.querySelector('a[href="/settings"]');
  92. expect(settingsLink).toBeInTheDocument();
  93. });
  94. });
  95. });
  96. describe('version display', () => {
  97. it('shows version info', async () => {
  98. render(<Layout />);
  99. await waitFor(() => {
  100. // Version info is displayed in sidebar
  101. expect(document.body).toBeInTheDocument();
  102. });
  103. });
  104. });
  105. describe('theme toggle', () => {
  106. it('has theme toggle button', async () => {
  107. render(<Layout />);
  108. await waitFor(() => {
  109. // Theme toggle should be present
  110. const buttons = document.querySelectorAll('button');
  111. expect(buttons.length).toBeGreaterThan(0);
  112. });
  113. });
  114. });
  115. describe('plate detection alert modal', () => {
  116. it('shows modal when plate-not-empty event is dispatched', async () => {
  117. render(<Layout />);
  118. // Dispatch the plate-not-empty event
  119. window.dispatchEvent(
  120. new CustomEvent('plate-not-empty', {
  121. detail: {
  122. printer_id: 1,
  123. printer_name: 'Test Printer',
  124. message: 'Objects detected on build plate',
  125. },
  126. })
  127. );
  128. await waitFor(() => {
  129. // Modal should appear with "Print Paused!" text
  130. expect(document.body.textContent).toContain('Print Paused!');
  131. expect(document.body.textContent).toContain('Test Printer');
  132. });
  133. });
  134. it('closes modal when I Understand button is clicked', async () => {
  135. render(<Layout />);
  136. // Dispatch the plate-not-empty event
  137. window.dispatchEvent(
  138. new CustomEvent('plate-not-empty', {
  139. detail: {
  140. printer_id: 1,
  141. printer_name: 'Test Printer',
  142. message: 'Objects detected on build plate',
  143. },
  144. })
  145. );
  146. await waitFor(() => {
  147. expect(document.body.textContent).toContain('Print Paused!');
  148. });
  149. // Click the "I Understand" button
  150. const button = document.querySelector('button');
  151. if (button && button.textContent?.includes('I Understand')) {
  152. button.click();
  153. }
  154. // Find and click the "I Understand" button by searching all buttons
  155. const buttons = document.querySelectorAll('button');
  156. buttons.forEach((btn) => {
  157. if (btn.textContent?.includes('I Understand')) {
  158. btn.click();
  159. }
  160. });
  161. await waitFor(() => {
  162. // Modal should be closed
  163. expect(document.body.textContent).not.toContain('Print Paused!');
  164. });
  165. });
  166. });
  167. describe('developer mode warning banner', () => {
  168. it('shows warning banner when printers lack developer mode', async () => {
  169. server.use(
  170. http.get('/api/v1/printers/developer-mode-warnings', () => {
  171. return HttpResponse.json([
  172. { printer_id: 1, name: 'X1 Carbon' },
  173. ]);
  174. })
  175. );
  176. render(<Layout />);
  177. await waitFor(() => {
  178. expect(document.body.textContent).toContain('Developer LAN mode is not enabled on');
  179. expect(document.body.textContent).toContain('X1 Carbon');
  180. });
  181. });
  182. it('shows multiple printer names in warning banner', async () => {
  183. server.use(
  184. http.get('/api/v1/printers/developer-mode-warnings', () => {
  185. return HttpResponse.json([
  186. { printer_id: 1, name: 'X1 Carbon' },
  187. { printer_id: 2, name: 'P1S' },
  188. ]);
  189. })
  190. );
  191. render(<Layout />);
  192. await waitFor(() => {
  193. expect(document.body.textContent).toContain('X1 Carbon');
  194. expect(document.body.textContent).toContain('P1S');
  195. });
  196. });
  197. it('hides warning banner when no printers lack developer mode', async () => {
  198. // Default handler returns empty array
  199. render(<Layout />);
  200. await waitFor(() => {
  201. const sidebar = document.querySelector('aside');
  202. expect(sidebar).toBeInTheDocument();
  203. });
  204. // Banner should not be present
  205. expect(document.body.textContent).not.toContain('Developer LAN mode is not enabled on');
  206. });
  207. it('shows how to enable link in warning banner', async () => {
  208. server.use(
  209. http.get('/api/v1/printers/developer-mode-warnings', () => {
  210. return HttpResponse.json([
  211. { printer_id: 1, name: 'X1 Carbon' },
  212. ]);
  213. })
  214. );
  215. render(<Layout />);
  216. await waitFor(() => {
  217. expect(document.body.textContent).toContain('How to enable');
  218. const link = document.querySelector('a[href*="enable-developer-mode"]');
  219. expect(link).toBeInTheDocument();
  220. });
  221. });
  222. });
  223. });