handlers.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { http, HttpResponse } from 'msw'
  2. import type { PatternMetadata, PreviewData } from '@/lib/types'
  3. // ============================================
  4. // API Call Tracking for Integration Tests
  5. // ============================================
  6. // Track API calls for integration test verification
  7. export const apiCallLog: Array<{
  8. endpoint: string
  9. method: string
  10. body?: unknown
  11. timestamp: number
  12. }> = []
  13. export function resetApiCallLog() {
  14. apiCallLog.length = 0
  15. }
  16. // Helper to log API calls
  17. function logApiCall(endpoint: string, method: string, body?: unknown) {
  18. apiCallLog.push({
  19. endpoint,
  20. method,
  21. body,
  22. timestamp: Date.now(),
  23. })
  24. }
  25. // ============================================
  26. // Mock Data Store (mutable for test scenarios)
  27. // ============================================
  28. export const mockData = {
  29. patterns: [
  30. { path: 'patterns/star.thr', name: 'star.thr', category: 'geometric', date_modified: Date.now(), coordinates_count: 150 },
  31. { path: 'patterns/spiral.thr', name: 'spiral.thr', category: 'organic', date_modified: Date.now() - 86400000, coordinates_count: 200 },
  32. { path: 'patterns/wave.thr', name: 'wave.thr', category: 'organic', date_modified: Date.now() - 172800000, coordinates_count: 175 },
  33. { path: 'patterns/custom/my_design.thr', name: 'my_design.thr', category: 'custom', date_modified: Date.now() - 259200000, coordinates_count: 100 },
  34. ] as PatternMetadata[],
  35. playlists: {
  36. default: ['patterns/star.thr', 'patterns/spiral.thr'],
  37. favorites: ['patterns/star.thr'],
  38. geometric: ['patterns/star.thr', 'patterns/wave.thr'],
  39. } as Record<string, string[]>,
  40. status: {
  41. is_running: false,
  42. is_paused: false,
  43. current_file: null as string | null,
  44. speed: 100,
  45. progress: 0,
  46. playlist_mode: false,
  47. playlist_name: null as string | null,
  48. queue: [] as string[],
  49. connection_status: 'connected',
  50. theta: 0,
  51. rho: 0.5,
  52. },
  53. }
  54. // Reset mock data between tests
  55. export function resetMockData() {
  56. mockData.patterns = [
  57. { path: 'patterns/star.thr', name: 'star.thr', category: 'geometric', date_modified: Date.now(), coordinates_count: 150 },
  58. { path: 'patterns/spiral.thr', name: 'spiral.thr', category: 'organic', date_modified: Date.now() - 86400000, coordinates_count: 200 },
  59. { path: 'patterns/wave.thr', name: 'wave.thr', category: 'organic', date_modified: Date.now() - 172800000, coordinates_count: 175 },
  60. { path: 'patterns/custom/my_design.thr', name: 'my_design.thr', category: 'custom', date_modified: Date.now() - 259200000, coordinates_count: 100 },
  61. ]
  62. mockData.playlists = {
  63. default: ['patterns/star.thr', 'patterns/spiral.thr'],
  64. favorites: ['patterns/star.thr'],
  65. geometric: ['patterns/star.thr', 'patterns/wave.thr'],
  66. }
  67. mockData.status = {
  68. is_running: false,
  69. is_paused: false,
  70. current_file: null,
  71. speed: 100,
  72. progress: 0,
  73. playlist_mode: false,
  74. playlist_name: null,
  75. queue: [],
  76. connection_status: 'connected',
  77. theta: 0,
  78. rho: 0.5,
  79. }
  80. }
  81. // ============================================
  82. // Handlers
  83. // ============================================
  84. export const handlers = [
  85. // ----------------
  86. // Pattern Endpoints
  87. // ----------------
  88. http.get('/list_theta_rho_files', () => {
  89. return HttpResponse.json(mockData.patterns.map(p => ({ name: p.name, path: p.path })))
  90. }),
  91. http.get('/list_theta_rho_files_with_metadata', () => {
  92. return HttpResponse.json(mockData.patterns)
  93. }),
  94. http.post('/preview_thr_batch', async ({ request }) => {
  95. const body = await request.json() as { files?: string[]; file_names?: string[] }
  96. const files = body.files || body.file_names || []
  97. const previews: Record<string, PreviewData> = {}
  98. for (const file of files) {
  99. previews[file] = {
  100. image_data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
  101. first_coordinate: { x: 0, y: 0 },
  102. last_coordinate: { x: 100, y: 100 },
  103. }
  104. }
  105. return HttpResponse.json(previews)
  106. }),
  107. http.post('/get_theta_rho_coordinates', async () => {
  108. // Return mock coordinates for pattern preview
  109. return HttpResponse.json({
  110. coordinates: Array.from({ length: 50 }, (_, i) => ({
  111. theta: i * 7.2,
  112. rho: 0.5 + Math.sin(i * 0.2) * 0.3,
  113. })),
  114. })
  115. }),
  116. http.post('/run_theta_rho', async ({ request }) => {
  117. const body = await request.json() as { file_name?: string; file?: string; pre_execution?: string }
  118. const file = body.file_name || body.file
  119. logApiCall('/run_theta_rho', 'POST', body)
  120. mockData.status.is_running = true
  121. mockData.status.current_file = file || null
  122. return HttpResponse.json({ success: true })
  123. }),
  124. http.post('/delete_theta_rho_file', async ({ request }) => {
  125. const body = await request.json() as { file_path: string }
  126. mockData.patterns = mockData.patterns.filter(p => p.path !== body.file_path)
  127. return HttpResponse.json({ success: true })
  128. }),
  129. http.post('/upload_theta_rho', async () => {
  130. return HttpResponse.json({ success: true, path: 'patterns/custom/uploaded.thr' })
  131. }),
  132. http.get('/api/pattern_history_all', () => {
  133. return HttpResponse.json({})
  134. }),
  135. http.get('/api/pattern_history/:path', () => {
  136. return HttpResponse.json({ executions: [] })
  137. }),
  138. // ----------------
  139. // Playlist Endpoints
  140. // ----------------
  141. http.get('/list_all_playlists', () => {
  142. return HttpResponse.json(Object.keys(mockData.playlists))
  143. }),
  144. http.get('/get_playlist', ({ request }) => {
  145. const url = new URL(request.url)
  146. const name = url.searchParams.get('name')
  147. if (name && mockData.playlists[name]) {
  148. return HttpResponse.json({ name, files: mockData.playlists[name] })
  149. }
  150. return HttpResponse.json({ name: name || '', files: [] })
  151. }),
  152. http.post('/create_playlist', async ({ request }) => {
  153. const body = await request.json() as { name: string; playlist_name?: string; files?: string[] }
  154. const name = body.playlist_name || body.name
  155. logApiCall('/create_playlist', 'POST', body)
  156. mockData.playlists[name] = body.files || []
  157. return HttpResponse.json({ success: true })
  158. }),
  159. http.post('/modify_playlist', async ({ request }) => {
  160. const body = await request.json() as { name?: string; playlist_name?: string; files: string[] }
  161. const name = body.playlist_name || body.name || ''
  162. logApiCall('/modify_playlist', 'POST', body)
  163. if (mockData.playlists[name]) {
  164. mockData.playlists[name] = body.files
  165. }
  166. return HttpResponse.json({ success: true })
  167. }),
  168. http.post('/rename_playlist', async ({ request }) => {
  169. const body = await request.json() as { old_name: string; new_name: string }
  170. logApiCall('/rename_playlist', 'POST', body)
  171. if (mockData.playlists[body.old_name]) {
  172. mockData.playlists[body.new_name] = mockData.playlists[body.old_name]
  173. delete mockData.playlists[body.old_name]
  174. }
  175. return HttpResponse.json({ success: true })
  176. }),
  177. http.delete('/delete_playlist', async ({ request }) => {
  178. const body = await request.json() as { name?: string; playlist_name?: string }
  179. const name = body.playlist_name || body.name || ''
  180. logApiCall('/delete_playlist', 'DELETE', body)
  181. delete mockData.playlists[name]
  182. return HttpResponse.json({ success: true })
  183. }),
  184. http.post('/run_playlist', async ({ request }) => {
  185. const body = await request.json() as { name?: string; playlist_name?: string }
  186. const name = body.playlist_name || body.name || ''
  187. logApiCall('/run_playlist', 'POST', body)
  188. const playlist = mockData.playlists[name]
  189. if (playlist && playlist.length > 0) {
  190. mockData.status.is_running = true
  191. mockData.status.playlist_mode = true
  192. mockData.status.playlist_name = name
  193. mockData.status.current_file = playlist[0]
  194. mockData.status.queue = playlist.slice(1)
  195. }
  196. return HttpResponse.json({ success: true })
  197. }),
  198. http.post('/add_to_playlist', async ({ request }) => {
  199. const body = await request.json() as { playlist_name: string; file_path: string }
  200. if (!mockData.playlists[body.playlist_name]) {
  201. mockData.playlists[body.playlist_name] = []
  202. }
  203. mockData.playlists[body.playlist_name].push(body.file_path)
  204. return HttpResponse.json({ success: true })
  205. }),
  206. // ----------------
  207. // Playback Control Endpoints
  208. // ----------------
  209. http.post('/pause_execution', () => {
  210. logApiCall('/pause_execution', 'POST')
  211. mockData.status.is_paused = true
  212. return HttpResponse.json({ success: true })
  213. }),
  214. http.post('/resume_execution', () => {
  215. logApiCall('/resume_execution', 'POST')
  216. mockData.status.is_paused = false
  217. return HttpResponse.json({ success: true })
  218. }),
  219. http.post('/stop_execution', () => {
  220. logApiCall('/stop_execution', 'POST')
  221. mockData.status.is_running = false
  222. mockData.status.is_paused = false
  223. mockData.status.current_file = null
  224. mockData.status.playlist_mode = false
  225. mockData.status.playlist_name = null
  226. mockData.status.queue = []
  227. return HttpResponse.json({ success: true })
  228. }),
  229. http.post('/force_stop', () => {
  230. mockData.status.is_running = false
  231. mockData.status.is_paused = false
  232. mockData.status.current_file = null
  233. mockData.status.playlist_mode = false
  234. mockData.status.queue = []
  235. return HttpResponse.json({ success: true })
  236. }),
  237. http.post('/skip_pattern', () => {
  238. logApiCall('/skip_pattern', 'POST')
  239. if (mockData.status.queue.length > 0) {
  240. mockData.status.current_file = mockData.status.queue.shift() || null
  241. } else {
  242. mockData.status.is_running = false
  243. mockData.status.current_file = null
  244. }
  245. return HttpResponse.json({ success: true })
  246. }),
  247. http.post('/set_speed', async ({ request }) => {
  248. const body = await request.json() as { speed: number }
  249. mockData.status.speed = body.speed
  250. return HttpResponse.json({ success: true })
  251. }),
  252. // ----------------
  253. // Table Control Endpoints
  254. // ----------------
  255. http.post('/send_home', () => {
  256. mockData.status.theta = 0
  257. mockData.status.rho = 0
  258. return HttpResponse.json({ success: true })
  259. }),
  260. http.post('/soft_reset', () => {
  261. return HttpResponse.json({ success: true })
  262. }),
  263. http.post('/move_to_center', () => {
  264. mockData.status.rho = 0
  265. return HttpResponse.json({ success: true })
  266. }),
  267. http.post('/move_to_perimeter', () => {
  268. mockData.status.rho = 1
  269. return HttpResponse.json({ success: true })
  270. }),
  271. http.post('/send_coordinate', async ({ request }) => {
  272. const body = await request.json() as { theta: number; rho: number }
  273. mockData.status.theta = body.theta
  274. mockData.status.rho = body.rho
  275. return HttpResponse.json({ success: true })
  276. }),
  277. // ----------------
  278. // Status Endpoints
  279. // ----------------
  280. http.get('/serial_status', () => {
  281. return HttpResponse.json(mockData.status)
  282. }),
  283. http.get('/list_serial_ports', () => {
  284. return HttpResponse.json(['/dev/ttyUSB0', '/dev/ttyUSB1'])
  285. }),
  286. // Board diagnostics
  287. http.get('/api/board/logs', () => {
  288. return HttpResponse.json({ lines: [] })
  289. }),
  290. http.get('/api/board/bootlog', () => {
  291. return HttpResponse.json({ text: '[+0] boot ok\n' })
  292. }),
  293. http.get('/api/board/coredump', () => {
  294. return HttpResponse.json({ present: false })
  295. }),
  296. http.post('/api/board/restart', () => {
  297. return HttpResponse.json({ success: true, rebooting: true })
  298. }),
  299. // Debug serial endpoints
  300. http.post('/api/debug-serial/open', () => {
  301. return HttpResponse.json({ success: true })
  302. }),
  303. http.post('/api/debug-serial/close', () => {
  304. return HttpResponse.json({ success: true })
  305. }),
  306. http.post('/api/debug-serial/send', () => {
  307. return HttpResponse.json({ success: true, response: 'OK' })
  308. }),
  309. ]