/** * File details entry points and indicators on the File Manager (#3077). * * The card kebab and the list row both offer "File details"; a card or row * shows a globe (external link, opens in a new tab), a sticky-note icon * (has notes) and a photo count only when the listing says so. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../utils'; import { FileManagerPage } from '../../pages/FileManagerPage'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; vi.mock('../../components/LibraryFileDetailsModal', () => ({ LibraryFileDetailsModal: ({ file }: { file: { filename: string } }) => (
{file.filename}
), })); const base = { file_path: '/library/x', file_size: 1048576, folder_id: null, thumbnail_path: null, print_name: null, print_time_seconds: null, print_count: 0, duplicate_count: 0, created_at: '2024-01-01T00:00:00Z', }; const files = [ { ...base, id: 1, filename: 'documented.stl', file_type: 'stl', external_url: 'https://www.printables.com/model/1', has_notes: true, photo_count: 3, }, { ...base, id: 2, filename: 'bare.stl', file_type: 'stl', external_url: null, has_notes: false, photo_count: 0 }, ]; function serve() { server.use( http.get('/api/v1/library/folders', () => HttpResponse.json([])), http.get('/api/v1/library/files', () => HttpResponse.json(files)), http.get('/api/v1/library/stats', () => HttpResponse.json({ total_files: files.length, total_folders: 0, total_size_bytes: 1, disk_free_bytes: 1, disk_total_bytes: 2, }), ), ); } function cardFor(filename: string): HTMLElement { return screen.getByText(filename).closest('.group') as HTMLElement; } function rowFor(filename: string): HTMLElement { return screen.getByText(filename).closest('div[class*="grid-cols-"]') as HTMLElement; } describe('FileManagerPage — file details (#3077)', () => { beforeEach(() => { serve(); }); afterEach(() => { (localStorage.getItem as ReturnType).mockReset(); }); describe('grid view', () => { it('opens the details modal from the card menu', async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getByText('bare.stl')).toBeInTheDocument()); const card = cardFor('bare.stl'); const kebab = card.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement; await user.click(kebab); await user.click(within(card).getByText('File details')); expect(await screen.findByTestId('details-modal')).toHaveTextContent('bare.stl'); }); it('shows the link, notes and photo indicators only when the listing carries them', async () => { render(); await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument()); const documented = cardFor('documented.stl'); const globe = within(documented).getByLabelText('Open link'); expect(globe).toHaveAttribute('href', 'https://www.printables.com/model/1'); expect(globe).toHaveAttribute('target', '_blank'); expect(within(documented).getByLabelText('Has notes')).toBeInTheDocument(); expect(within(documented).getByLabelText('3 photos')).toHaveTextContent('3'); const bare = cardFor('bare.stl'); expect(within(bare).queryByLabelText('Open link')).not.toBeInTheDocument(); expect(within(bare).queryByLabelText('Has notes')).not.toBeInTheDocument(); expect(within(bare).queryByLabelText(/photos?$/)).not.toBeInTheDocument(); }); it('offers the link in the card menu only when the file has one', async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument()); const documented = cardFor('documented.stl'); await user.click(documented.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement); expect(within(documented).getByText('Open link')).toBeInTheDocument(); await user.keyboard('{Escape}'); const bare = cardFor('bare.stl'); await user.click(bare.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement); expect(within(bare).getByText('File details')).toBeInTheDocument(); expect(within(bare).queryByText('Open link')).not.toBeInTheDocument(); }); it('opens the stored link without handing the new tab a window.opener', async () => { // The URL comes from whoever owns the file, so the page it opens must // not get a handle back on Bambuddy's window. const open = vi.spyOn(window, 'open').mockReturnValue(null); const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument()); const documented = cardFor('documented.stl'); await user.click(documented.querySelector('.lucide-ellipsis-vertical')?.closest('button') as HTMLButtonElement); await user.click(within(documented).getByText('Open link')); expect(open).toHaveBeenCalledWith('https://www.printables.com/model/1', '_blank', 'noopener,noreferrer'); open.mockRestore(); }); }); describe('list view', () => { beforeEach(() => { (localStorage.getItem as ReturnType).mockImplementation((key: string) => key === 'library-view-mode' ? 'list' : null, ); }); it('opens the details modal from the row action', async () => { const user = userEvent.setup(); render(); await waitFor(() => expect(screen.getByText('bare.stl')).toBeInTheDocument()); await user.click(within(rowFor('bare.stl')).getByTitle('File details')); expect(await screen.findByTestId('details-modal')).toHaveTextContent('bare.stl'); }); it('shows the indicators next to the name', async () => { render(); await waitFor(() => expect(screen.getByText('documented.stl')).toBeInTheDocument()); const documented = rowFor('documented.stl'); expect(within(documented).getByLabelText('Open link')).toHaveAttribute( 'href', 'https://www.printables.com/model/1', ); expect(within(documented).getByLabelText('Has notes')).toBeInTheDocument(); expect(within(documented).getByTitle('3 photos')).toHaveTextContent('3'); const bare = rowFor('bare.stl'); expect(within(bare).queryByLabelText('Open link')).not.toBeInTheDocument(); expect(within(bare).queryByLabelText('Has notes')).not.toBeInTheDocument(); }); }); });