ArchivesPage.test.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. /**
  2. * Tests for the ArchivesPage component.
  3. */
  4. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  5. import { screen, waitFor, fireEvent } from '@testing-library/react';
  6. import { render } from '../utils';
  7. import { ArchivesPage } from '../../pages/ArchivesPage';
  8. import { http, HttpResponse } from 'msw';
  9. import { server } from '../mocks/server';
  10. import { setAuthToken } from '../../api/client';
  11. const mockArchives = [
  12. {
  13. id: 1,
  14. filename: 'benchy.gcode.3mf',
  15. print_name: 'Benchy',
  16. printer_id: 1,
  17. printer_name: 'X1 Carbon',
  18. print_time_seconds: 3600,
  19. filament_used_grams: 15.5,
  20. status: 'completed',
  21. started_at: '2024-01-01T10:00:00Z',
  22. completed_at: '2024-01-01T11:00:00Z',
  23. thumbnail_path: '/thumbnails/1.png',
  24. notes: 'Test print',
  25. rating: 5,
  26. project_id: null,
  27. project_name: null,
  28. project_color: null,
  29. print_count: 3,
  30. tags: 'test,calibration',
  31. created_at: '2024-01-01T09:00:00Z',
  32. updated_at: '2024-01-01T11:00:00Z',
  33. has_f3d: false,
  34. },
  35. {
  36. id: 2,
  37. filename: 'bracket.gcode.3mf',
  38. print_name: 'Bracket v2',
  39. printer_id: 1,
  40. printer_name: 'X1 Carbon',
  41. print_time_seconds: 7200,
  42. filament_used_grams: 45.0,
  43. status: 'completed',
  44. started_at: '2024-01-02T14:00:00Z',
  45. completed_at: '2024-01-02T16:00:00Z',
  46. thumbnail_path: '/thumbnails/2.png',
  47. notes: null,
  48. rating: null,
  49. project_id: 1,
  50. project_name: 'Functional Parts',
  51. project_color: '#00ae42',
  52. print_count: 1,
  53. tags: '',
  54. created_at: '2024-01-02T13:00:00Z',
  55. updated_at: '2024-01-02T16:00:00Z',
  56. has_f3d: true,
  57. },
  58. ];
  59. const mockArchiveStats = {
  60. total_archives: 10,
  61. total_print_time_seconds: 36000,
  62. total_filament_grams: 500,
  63. prints_this_week: 5,
  64. prints_this_month: 20,
  65. };
  66. describe('ArchivesPage', () => {
  67. beforeEach(() => {
  68. setAuthToken(null);
  69. server.use(
  70. http.get('/api/v1/archives/', () => {
  71. return HttpResponse.json(mockArchives);
  72. }),
  73. http.get('/api/v1/archives/stats', () => {
  74. return HttpResponse.json(mockArchiveStats);
  75. }),
  76. http.get('/api/v1/printers/', () => {
  77. return HttpResponse.json([{ id: 1, name: 'X1 Carbon' }]);
  78. }),
  79. http.get('/api/v1/projects/', () => {
  80. return HttpResponse.json([{ id: 1, name: 'Functional Parts', color: '#00ae42' }]);
  81. }),
  82. http.get('/api/v1/archives/tags', () => {
  83. return HttpResponse.json(['test', 'calibration', 'functional']);
  84. }),
  85. http.get('/api/v1/archives/:id/plates', ({ params }) => {
  86. const archiveId = Number(params.id);
  87. return HttpResponse.json({
  88. archive_id: Number.isFinite(archiveId) ? archiveId : 0,
  89. filename: 'sample.3mf',
  90. plates: [],
  91. is_multi_plate: false,
  92. });
  93. }),
  94. http.get('/api/v1/archives/:id/filament-requirements', () => {
  95. return HttpResponse.json([]);
  96. }),
  97. http.delete('/api/v1/archives/:id', () => {
  98. return HttpResponse.json({ success: true });
  99. })
  100. );
  101. });
  102. afterEach(() => {
  103. setAuthToken(null);
  104. });
  105. describe('rendering', () => {
  106. it('renders the page title', async () => {
  107. render(<ArchivesPage />);
  108. await waitFor(() => {
  109. expect(screen.getByText('Archives')).toBeInTheDocument();
  110. });
  111. });
  112. it('shows archive cards', async () => {
  113. render(<ArchivesPage />);
  114. await waitFor(() => {
  115. expect(screen.getByText('Benchy')).toBeInTheDocument();
  116. expect(screen.getByText('Bracket v2')).toBeInTheDocument();
  117. });
  118. });
  119. });
  120. describe('archive info', () => {
  121. it('shows print time', async () => {
  122. render(<ArchivesPage />);
  123. await waitFor(() => {
  124. expect(screen.getByText('1h 0m')).toBeInTheDocument();
  125. });
  126. });
  127. it('shows printer name', async () => {
  128. render(<ArchivesPage />);
  129. await waitFor(() => {
  130. const printerNames = screen.getAllByText('X1 Carbon');
  131. expect(printerNames.length).toBeGreaterThan(0);
  132. });
  133. });
  134. it('shows tags', async () => {
  135. render(<ArchivesPage />);
  136. await waitFor(() => {
  137. // Tags may be truncated or displayed differently - just verify archives load
  138. expect(screen.getByText('Benchy')).toBeInTheDocument();
  139. });
  140. // Tags are displayed in the archive cards
  141. const testElements = screen.queryAllByText('test');
  142. expect(testElements.length).toBeGreaterThanOrEqual(0);
  143. });
  144. it('shows print count badge', async () => {
  145. render(<ArchivesPage />);
  146. await waitFor(() => {
  147. // Print count may be displayed as badge
  148. expect(screen.getByText('Benchy')).toBeInTheDocument();
  149. });
  150. });
  151. it('shows project badge', async () => {
  152. render(<ArchivesPage />);
  153. await waitFor(() => {
  154. expect(screen.getByText('Functional Parts')).toBeInTheDocument();
  155. });
  156. });
  157. it('shows F3D indicator when file has F3D', async () => {
  158. render(<ArchivesPage />);
  159. await waitFor(() => {
  160. // Bracket v2 has has_f3d: true
  161. expect(screen.getByText('Bracket v2')).toBeInTheDocument();
  162. });
  163. // F3D files have cyan badge indicator - look for it by title or class
  164. const f3dElements = document.querySelectorAll('[title*="F3D"]');
  165. expect(f3dElements.length).toBeGreaterThanOrEqual(0);
  166. });
  167. });
  168. describe('search and filter', () => {
  169. it('has search input', async () => {
  170. render(<ArchivesPage />);
  171. await waitFor(() => {
  172. expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
  173. });
  174. });
  175. it('has printer filter', async () => {
  176. render(<ArchivesPage />);
  177. await waitFor(() => {
  178. expect(screen.getByText('All Printers')).toBeInTheDocument();
  179. });
  180. });
  181. it('has project filter', async () => {
  182. render(<ArchivesPage />);
  183. await waitFor(() => {
  184. // Project filter dropdown may have different default text
  185. const projectSelect = screen.getAllByRole('combobox');
  186. expect(projectSelect.length).toBeGreaterThan(0);
  187. });
  188. });
  189. });
  190. describe('view modes', () => {
  191. it('has grid view option', async () => {
  192. render(<ArchivesPage />);
  193. await waitFor(() => {
  194. expect(screen.getByTitle(/grid/i)).toBeInTheDocument();
  195. });
  196. });
  197. it('has list view option', async () => {
  198. render(<ArchivesPage />);
  199. await waitFor(() => {
  200. expect(screen.getByTitle(/list/i)).toBeInTheDocument();
  201. });
  202. });
  203. });
  204. describe('empty state', () => {
  205. it('shows empty state when no archives', async () => {
  206. server.use(
  207. http.get('/api/v1/archives/', () => {
  208. return HttpResponse.json([]);
  209. })
  210. );
  211. render(<ArchivesPage />);
  212. await waitFor(() => {
  213. expect(screen.getByText(/no archives/i)).toBeInTheDocument();
  214. });
  215. });
  216. });
  217. describe('stats display', () => {
  218. it('shows archives list', async () => {
  219. render(<ArchivesPage />);
  220. await waitFor(() => {
  221. // Verify archives are loaded
  222. expect(screen.getByText('Benchy')).toBeInTheDocument();
  223. expect(screen.getByText('Bracket v2')).toBeInTheDocument();
  224. });
  225. });
  226. });
  227. describe('rating display', () => {
  228. it('shows rating stars', async () => {
  229. render(<ArchivesPage />);
  230. await waitFor(() => {
  231. // Rating 5 shows stars
  232. expect(screen.getByText('Benchy')).toBeInTheDocument();
  233. });
  234. });
  235. });
  236. describe('plate navigation', () => {
  237. it('renders archive cards with thumbnails', async () => {
  238. render(<ArchivesPage />);
  239. await waitFor(() => {
  240. // Archive cards should render with their thumbnails
  241. expect(screen.getByText('Benchy')).toBeInTheDocument();
  242. // Thumbnail images should be present (archive cards have img elements)
  243. const images = document.querySelectorAll('img[alt="Benchy"]');
  244. expect(images.length).toBeGreaterThanOrEqual(0);
  245. });
  246. });
  247. it('fetches plate data for multi-plate archives on hover', async () => {
  248. // Setup handler for plates endpoint
  249. server.use(
  250. http.get('/api/v1/archives/:id/plates', ({ params }) => {
  251. return HttpResponse.json({
  252. archive_id: Number(params.id),
  253. filename: 'test.3mf',
  254. plates: [
  255. { index: 0, name: 'Plate 1', objects: ['Object A'], has_thumbnail: true, thumbnail_url: '/thumb1.png', print_time_seconds: 3600, filament_used_grams: 10, filaments: [] },
  256. { index: 1, name: 'Plate 2', objects: ['Object B'], has_thumbnail: true, thumbnail_url: '/thumb2.png', print_time_seconds: 1800, filament_used_grams: 5, filaments: [] },
  257. ],
  258. is_multi_plate: true,
  259. });
  260. })
  261. );
  262. render(<ArchivesPage />);
  263. await waitFor(() => {
  264. expect(screen.getByText('Benchy')).toBeInTheDocument();
  265. });
  266. // Archives with multi-plate support will show navigation on hover
  267. // The plates API is called lazily when hovering
  268. });
  269. it('names the plate in the card title when it is not the first one', async () => {
  270. server.use(
  271. http.get('/api/v1/archives/', () =>
  272. HttpResponse.json([{ ...mockArchives[1], plate_id: 3 }])
  273. )
  274. );
  275. render(<ArchivesPage />);
  276. await waitFor(() => {
  277. expect(screen.getByText('Bracket v2 \u2014 Plate 3')).toBeInTheDocument();
  278. });
  279. });
  280. it('leaves the title alone for a print on plate 1', async () => {
  281. // The queue records a plate for single-plate files too, so an ungated
  282. // label reads "Plate 1" on ordinary prints (#2796).
  283. server.use(
  284. http.get('/api/v1/archives/', () =>
  285. HttpResponse.json([{ ...mockArchives[0], plate_id: 1 }])
  286. )
  287. );
  288. render(<ArchivesPage />);
  289. await waitFor(() => {
  290. expect(screen.getByText('Benchy')).toBeInTheDocument();
  291. });
  292. expect(screen.queryByText(/Plate 1/)).not.toBeInTheDocument();
  293. });
  294. });
  295. describe('timelapse management', () => {
  296. // A card reaches print-video downloads through its context menu; only the
  297. // list row keeps an action-bar button for it (#2853).
  298. const openCardPrintVideos = async (cardIndex = 0) => {
  299. const triggers = await screen.findAllByTitle('Right-click for more options');
  300. fireEvent.click(triggers[cardIndex]);
  301. return screen.findByRole('button', { name: 'Download print videos' });
  302. };
  303. it('keeps an attached timelapse available without printer-file permission', async () => {
  304. setAuthToken('archive-only-token', 'session');
  305. server.use(
  306. http.get('*/api/v1/auth/status', () => HttpResponse.json({
  307. auth_enabled: true,
  308. requires_setup: false,
  309. })),
  310. http.get('/api/v1/auth/me', () => HttpResponse.json({
  311. id: 7,
  312. username: 'archive-viewer',
  313. is_active: true,
  314. is_admin: false,
  315. groups: [],
  316. permissions: ['archives:read_all'],
  317. created_at: '2026-08-18T00:00:00Z',
  318. })),
  319. http.get('/api/v1/archives/', () => HttpResponse.json([
  320. { ...mockArchives[0], timelapse_path: 'timelapses/attached.mp4' },
  321. { ...mockArchives[1], timelapse_path: null },
  322. ])),
  323. http.get('/api/v1/archives/:id/printer-media', ({ params }) => HttpResponse.json({
  324. archive_id: Number(params.id),
  325. printer_id: 1,
  326. local_timelapse: { name: 'attached.mp4', size: 1024 },
  327. remote_files: [],
  328. warnings: ['printer_files_forbidden'],
  329. })),
  330. );
  331. render(<ArchivesPage />);
  332. // One of the two archives has no attached copy, so without printer-file
  333. // permission there is nothing left for the action to offer.
  334. const deniedItem = await openCardPrintVideos(0);
  335. expect(deniedItem).toBeDisabled();
  336. expect(deniedItem).toHaveAttribute('title', 'You do not have permission to access printer files');
  337. fireEvent.keyDown(document, { key: 'Escape' });
  338. const mediaItem = await openCardPrintVideos(1);
  339. expect(mediaItem).toBeEnabled();
  340. fireEvent.click(mediaItem);
  341. expect(await screen.findByText('attached.mp4')).toBeInTheDocument();
  342. expect(screen.getByText('You do not have permission to access printer files')).toBeInTheDocument();
  343. });
  344. it('opens print video downloads and shows matching IP camera chunks', async () => {
  345. server.use(
  346. http.get('/api/v1/archives/:id/printer-media', ({ params }) => {
  347. return HttpResponse.json({
  348. archive_id: Number(params.id),
  349. printer_id: 1,
  350. local_timelapse: null,
  351. remote_files: [
  352. {
  353. name: 'ipcam-record.2024-01-01_10-05-00.1.mp4',
  354. path: '/ipcam/ipcam-record.2024-01-01_10-05-00.1.mp4',
  355. size: 250_000_000,
  356. mtime: '2024-01-01T10:10:00Z',
  357. kind: 'ipcam',
  358. },
  359. ],
  360. warnings: [],
  361. });
  362. }),
  363. );
  364. render(<ArchivesPage />);
  365. fireEvent.click(await openCardPrintVideos());
  366. expect(await screen.findByText('Print videos')).toBeInTheDocument();
  367. expect(await screen.findByText('ipcam-record.2024-01-01_10-05-00.1.mp4')).toBeInTheDocument();
  368. });
  369. it('shows a toast when printer video ZIP preparation fails', async () => {
  370. server.use(
  371. http.get('/api/v1/archives/:id/printer-media', ({ params }) => HttpResponse.json({
  372. archive_id: Number(params.id),
  373. printer_id: 1,
  374. local_timelapse: null,
  375. remote_files: [{
  376. name: 'ipcam-record.1.mp4',
  377. path: '/ipcam/ipcam-record.1.mp4',
  378. size: 250_000_000,
  379. mtime: '2024-01-01T10:10:00Z',
  380. kind: 'ipcam',
  381. }],
  382. warnings: [],
  383. })),
  384. http.post('/api/v1/printers/:id/files/download-job', () => HttpResponse.json({
  385. job_id: 'failed-job',
  386. state: 'failed',
  387. requested: 1,
  388. successful: 0,
  389. failed: 0,
  390. token: null,
  391. message: 'Not enough app data volume space',
  392. })),
  393. );
  394. render(<ArchivesPage />);
  395. fireEvent.click(await openCardPrintVideos());
  396. fireEvent.click(await screen.findByRole('button', { name: /Download selected \(1\)/i }));
  397. expect(await screen.findByText('Download failed: Not enough app data volume space')).toBeInTheDocument();
  398. });
  399. it('shows upload timelapse menu item when no timelapse attached', async () => {
  400. const archivesWithoutTimelapse = mockArchives.map(a => ({ ...a, timelapse_path: null }));
  401. server.use(
  402. http.get('/api/v1/archives/', () => {
  403. return HttpResponse.json(archivesWithoutTimelapse);
  404. })
  405. );
  406. render(<ArchivesPage />);
  407. await waitFor(() => {
  408. expect(screen.getByText('Benchy')).toBeInTheDocument();
  409. });
  410. // Context menu items are rendered in the DOM even when not visible
  411. // "Upload Timelapse" should be present for archives without timelapse
  412. const uploadItems = screen.queryAllByText('Upload Timelapse');
  413. expect(uploadItems.length).toBeGreaterThanOrEqual(0);
  414. });
  415. it('shows remove timelapse menu item when timelapse is attached', async () => {
  416. const archivesWithTimelapse = mockArchives.map(a => ({
  417. ...a,
  418. timelapse_path: 'archives/1/timelapse.mp4',
  419. }));
  420. server.use(
  421. http.get('/api/v1/archives/', () => {
  422. return HttpResponse.json(archivesWithTimelapse);
  423. })
  424. );
  425. render(<ArchivesPage />);
  426. await waitFor(() => {
  427. expect(screen.getByText('Benchy')).toBeInTheDocument();
  428. });
  429. // "Remove Timelapse" should be present for archives with timelapse
  430. const removeItems = screen.queryAllByText('Remove Timelapse');
  431. expect(removeItems.length).toBeGreaterThanOrEqual(0);
  432. });
  433. it('disables scan for timelapse when timelapse is already attached', async () => {
  434. const archivesWithTimelapse = mockArchives.map(a => ({
  435. ...a,
  436. timelapse_path: 'archives/1/timelapse.mp4',
  437. }));
  438. server.use(
  439. http.get('/api/v1/archives/', () => {
  440. return HttpResponse.json(archivesWithTimelapse);
  441. })
  442. );
  443. render(<ArchivesPage />);
  444. await waitFor(() => {
  445. expect(screen.getByText('Benchy')).toBeInTheDocument();
  446. });
  447. // "Scan for Timelapse" buttons should be disabled when timelapse exists
  448. // Upload Timelapse should also be disabled
  449. });
  450. });
  451. // #1153 — Sylvain wanted to differentiate VP-uploaded archives (status='archived',
  452. // never sent to a printer) from those that have been printed at least once.
  453. describe('add-to-project submenu (#2888)', () => {
  454. // The submenu used to offer active projects only, which left a completed
  455. // project unreachable from here while the Edit dialog still offered it.
  456. // Both now hide archived projects and nothing else.
  457. beforeEach(() => {
  458. server.use(
  459. http.get('/api/v1/projects/', () =>
  460. HttpResponse.json([
  461. { id: 1, name: 'Functional Parts', color: '#00ae42', status: 'active' },
  462. { id: 2, name: 'Shipped Last Month', color: '#0088ff', status: 'completed' },
  463. { id: 3, name: 'Season 2025', color: '#888888', status: 'archived' },
  464. ]),
  465. ),
  466. );
  467. });
  468. const openSubmenu = async () => {
  469. const card = await screen.findByText('Benchy');
  470. fireEvent.contextMenu(card);
  471. fireEvent.click(await screen.findByText('Add to Project'));
  472. };
  473. it('offers a completed project', async () => {
  474. render(<ArchivesPage />);
  475. await openSubmenu();
  476. expect(await screen.findByText('Shipped Last Month')).toBeInTheDocument();
  477. });
  478. it('leaves archived projects out', async () => {
  479. render(<ArchivesPage />);
  480. await openSubmenu();
  481. // Anchored on the completed one rather than the active one: the active
  482. // project's name is also drawn on an archive card behind the menu.
  483. await screen.findByText('Shipped Last Month');
  484. expect(screen.queryByText('Season 2025')).not.toBeInTheDocument();
  485. });
  486. });
  487. describe('Not Printed / Printed collections', () => {
  488. const mixedStatusArchives = [
  489. { ...mockArchives[0], id: 100, print_name: 'NeverPrinted', status: 'archived', started_at: null, completed_at: null },
  490. { ...mockArchives[0], id: 101, print_name: 'WasPrinted', status: 'completed' },
  491. { ...mockArchives[0], id: 102, print_name: 'WasFailed', status: 'failed' },
  492. { ...mockArchives[0], id: 103, print_name: 'WasCancelled', status: 'cancelled' },
  493. ];
  494. beforeEach(() => {
  495. // Reset persisted collection so each test starts on "All Archives".
  496. window.localStorage.removeItem('archiveCollection');
  497. server.use(
  498. http.get('/api/v1/archives/', () => HttpResponse.json(mixedStatusArchives))
  499. );
  500. });
  501. it('shows only archived (never-printed) entries when "Not Printed" is selected', async () => {
  502. render(<ArchivesPage />);
  503. await waitFor(() => {
  504. expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
  505. });
  506. const collectionSelect = screen.getByDisplayValue('All Archives');
  507. fireEvent.change(collectionSelect, { target: { value: 'not-printed' } });
  508. await waitFor(() => {
  509. expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
  510. expect(screen.queryByText('WasPrinted')).not.toBeInTheDocument();
  511. expect(screen.queryByText('WasFailed')).not.toBeInTheDocument();
  512. expect(screen.queryByText('WasCancelled')).not.toBeInTheDocument();
  513. });
  514. });
  515. it('shows only print-attempted entries (any final status) when "Printed" is selected', async () => {
  516. render(<ArchivesPage />);
  517. await waitFor(() => {
  518. expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
  519. });
  520. const collectionSelect = screen.getByDisplayValue('All Archives');
  521. fireEvent.change(collectionSelect, { target: { value: 'printed' } });
  522. await waitFor(() => {
  523. expect(screen.queryByText('NeverPrinted')).not.toBeInTheDocument();
  524. expect(screen.getByText('WasPrinted')).toBeInTheDocument();
  525. expect(screen.getByText('WasFailed')).toBeInTheDocument();
  526. expect(screen.getByText('WasCancelled')).toBeInTheDocument();
  527. });
  528. });
  529. });
  530. // #1898: the "outcome?" badge on a print still waiting for its verdict opens
  531. // the dialog. The PATCH behind it checks archives:update against the owner,
  532. // like the menu item beside it, so for anyone else the badge is a status only.
  533. describe('awaiting-outcome badge', () => {
  534. const awaiting = { ...mockArchives[0], confirm_requested: true, user_verdict: null, created_by_id: 7 };
  535. const signInWith = (permissions: string[]) => {
  536. setAuthToken('verdict-token', 'session');
  537. server.use(
  538. http.get('*/api/v1/auth/status', () => HttpResponse.json({ auth_enabled: true, requires_setup: false })),
  539. http.get('/api/v1/auth/me', () => HttpResponse.json({
  540. id: 7,
  541. username: 'operator',
  542. is_active: true,
  543. is_admin: false,
  544. groups: [],
  545. permissions,
  546. created_at: '2026-08-18T00:00:00Z',
  547. })),
  548. http.get('/api/v1/archives/', () => HttpResponse.json([awaiting])),
  549. );
  550. };
  551. afterEach(() => {
  552. window.localStorage.removeItem('archiveViewMode');
  553. });
  554. it('is a status only for a user who may not update the archive', async () => {
  555. signInWith(['archives:read_all']);
  556. render(<ArchivesPage />);
  557. const badge = await screen.findByRole('button', { name: 'outcome?' });
  558. await waitFor(() => expect(badge).toBeDisabled());
  559. expect(badge).toHaveAttribute('title', 'You do not have permission to update archives');
  560. fireEvent.click(badge);
  561. expect(screen.queryByTestId('confirm-outcome-dialog')).toBeNull();
  562. });
  563. it('is a status only in the list view too', async () => {
  564. window.localStorage.setItem('archiveViewMode', 'list');
  565. signInWith(['archives:read_all']);
  566. render(<ArchivesPage />);
  567. const badge = await screen.findByRole('button', { name: 'outcome?' });
  568. await waitFor(() => expect(badge).toBeDisabled());
  569. expect(badge).toHaveAttribute('title', 'You do not have permission to update archives');
  570. });
  571. it('is disabled on somebody else\'s print for a user who may update only their own', async () => {
  572. signInWith(['archives:read_all', 'archives:update_own']);
  573. server.use(http.get('/api/v1/archives/', () => HttpResponse.json([{ ...awaiting, created_by_id: 8 }])));
  574. render(<ArchivesPage />);
  575. const badge = await screen.findByRole('button', { name: 'outcome?' });
  576. await waitFor(() => expect(badge).toBeDisabled());
  577. });
  578. it('opens the dialog for the owner', async () => {
  579. signInWith(['archives:read_all', 'archives:update_own']);
  580. render(<ArchivesPage />);
  581. const badge = await screen.findByRole('button', { name: 'outcome?' });
  582. await waitFor(() => expect(badge).toBeEnabled());
  583. fireEvent.click(badge);
  584. expect(await screen.findByTestId('confirm-outcome-dialog')).toBeInTheDocument();
  585. });
  586. });
  587. });