FileManagerFileDetails.test.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * File details entry points and indicators on the File Manager (#3077).
  3. *
  4. * The card kebab and the list row both offer "File details"; a card or row
  5. * shows a globe (external link, opens in a new tab), a sticky-note icon
  6. * (has notes) and a photo count only when the listing says so.
  7. */
  8. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  9. import { screen, waitFor, within } from '@testing-library/react';
  10. import userEvent from '@testing-library/user-event';
  11. import { render } from '../utils';
  12. import { FileManagerPage } from '../../pages/FileManagerPage';
  13. import { http, HttpResponse } from 'msw';
  14. import { server } from '../mocks/server';
  15. vi.mock('../../components/LibraryFileDetailsModal', () => ({
  16. LibraryFileDetailsModal: ({ file }: { file: { filename: string } }) => (
  17. <div data-testid="details-modal">{file.filename}</div>
  18. ),
  19. }));
  20. const base = {
  21. file_path: '/library/x',
  22. file_size: 1048576,
  23. folder_id: null,
  24. thumbnail_path: null,
  25. print_name: null,
  26. print_time_seconds: null,
  27. print_count: 0,
  28. duplicate_count: 0,
  29. created_at: '2024-01-01T00:00:00Z',
  30. };
  31. const files = [
  32. {
  33. ...base,
  34. id: 1,
  35. filename: 'documented.stl',
  36. file_type: 'stl',
  37. external_url: 'https://www.printables.com/model/1',
  38. has_notes: true,
  39. photo_count: 3,
  40. },
  41. { ...base, id: 2, filename: 'bare.stl', file_type: 'stl', external_url: null, has_notes: false, photo_count: 0 },
  42. ];
  43. function serve() {
  44. server.use(
  45. http.get('/api/v1/library/folders', () => HttpResponse.json([])),
  46. http.get('/api/v1/library/files', () => HttpResponse.json(files)),
  47. http.get('/api/v1/library/stats', () =>
  48. HttpResponse.json({
  49. total_files: files.length,
  50. total_folders: 0,
  51. total_size_bytes: 1,
  52. disk_free_bytes: 1,
  53. disk_total_bytes: 2,
  54. }),
  55. ),
  56. );
  57. }
  58. function cardFor(filename: string): HTMLElement {
  59. return screen.getByText(filename).closest('.group') as HTMLElement;
  60. }
  61. function rowFor(filename: string): HTMLElement {
  62. return screen.getByText(filename).closest('div[class*="grid-cols-"]') as HTMLElement;
  63. }
  64. describe('FileManagerPage — file details (#3077)', () => {
  65. beforeEach(() => {
  66. serve();
  67. });
  68. afterEach(() => {
  69. (localStorage.getItem as ReturnType<typeof vi.fn>).mockReset();
  70. });
  71. describe('grid view', () => {
  72. it('opens the details modal from the card menu', async () => {
  73. const user = userEvent.setup();
  74. render(<FileManagerPage />);
  75. await waitFor(() => expect(screen.getByText('bare.stl')).toBeInTheDocument());
  76. const card = cardFor('bare.stl');
  77. const kebab = card.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement;
  78. await user.click(kebab);
  79. await user.click(within(card).getByText('File details'));
  80. expect(await screen.findByTestId('details-modal')).toHaveTextContent('bare.stl');
  81. });
  82. it('shows the link, notes and photo indicators only when the listing carries them', async () => {
  83. render(<FileManagerPage />);
  84. await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument());
  85. const documented = cardFor('documented.stl');
  86. const globe = within(documented).getByLabelText('Open link');
  87. expect(globe).toHaveAttribute('href', 'https://www.printables.com/model/1');
  88. expect(globe).toHaveAttribute('target', '_blank');
  89. expect(within(documented).getByLabelText('Has notes')).toBeInTheDocument();
  90. expect(within(documented).getByLabelText('3 photos')).toHaveTextContent('3');
  91. const bare = cardFor('bare.stl');
  92. expect(within(bare).queryByLabelText('Open link')).not.toBeInTheDocument();
  93. expect(within(bare).queryByLabelText('Has notes')).not.toBeInTheDocument();
  94. expect(within(bare).queryByLabelText(/photos?$/)).not.toBeInTheDocument();
  95. });
  96. it('offers the link in the card menu only when the file has one', async () => {
  97. const user = userEvent.setup();
  98. render(<FileManagerPage />);
  99. await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument());
  100. const documented = cardFor('documented.stl');
  101. await user.click(documented.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement);
  102. expect(within(documented).getByText('Open link')).toBeInTheDocument();
  103. await user.keyboard('{Escape}');
  104. const bare = cardFor('bare.stl');
  105. await user.click(bare.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement);
  106. expect(within(bare).getByText('File details')).toBeInTheDocument();
  107. expect(within(bare).queryByText('Open link')).not.toBeInTheDocument();
  108. });
  109. it('opens the stored link without handing the new tab a window.opener', async () => {
  110. // The URL comes from whoever owns the file, so the page it opens must
  111. // not get a handle back on Bambuddy's window.
  112. const open = vi.spyOn(window, 'open').mockReturnValue(null);
  113. const user = userEvent.setup();
  114. render(<FileManagerPage />);
  115. await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument());
  116. const documented = cardFor('documented.stl');
  117. await user.click(documented.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement);
  118. await user.click(within(documented).getByText('Open link'));
  119. expect(open).toHaveBeenCalledWith('https://www.printables.com/model/1', '_blank', 'noopener,noreferrer');
  120. open.mockRestore();
  121. });
  122. });
  123. describe('list view', () => {
  124. beforeEach(() => {
  125. (localStorage.getItem as ReturnType<typeof vi.fn>).mockImplementation((key: string) =>
  126. key === 'library-view-mode' ? 'list' : null,
  127. );
  128. });
  129. it('opens the details modal from the row action', async () => {
  130. const user = userEvent.setup();
  131. render(<FileManagerPage />);
  132. await waitFor(() => expect(screen.getByText('bare.stl')).toBeInTheDocument());
  133. await user.click(within(rowFor('bare.stl')).getByTitle('File details'));
  134. expect(await screen.findByTestId('details-modal')).toHaveTextContent('bare.stl');
  135. });
  136. it('shows the indicators next to the name', async () => {
  137. render(<FileManagerPage />);
  138. await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument());
  139. const documented = rowFor('documented.stl');
  140. expect(within(documented).getByLabelText('Open link')).toHaveAttribute(
  141. 'href',
  142. 'https://www.printables.com/model/1',
  143. );
  144. expect(within(documented).getByLabelText('Has notes')).toBeInTheDocument();
  145. expect(within(documented).getByTitle('3 photos')).toHaveTextContent('3');
  146. const bare = rowFor('bare.stl');
  147. expect(within(bare).queryByLabelText('Open link')).not.toBeInTheDocument();
  148. expect(within(bare).queryByLabelText('Has notes')).not.toBeInTheDocument();
  149. });
  150. });
  151. });