import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; import App from './App'; import { PREFERENCES_KEY } from './preferences'; import { catalogue, catalogueFetch } from './test/fixtures'; afterEach(() => vi.unstubAllGlobals()); describe('portal UI', () => { it('renders app cards and persists accessible pin/hide controls', async () => { vi.stubGlobal('fetch', vi.fn(catalogueFetch())); const user = userEvent.setup(); render(); expect( await screen.findByRole('heading', { name: 'PDF Workbench' }) ).toBeInTheDocument(); expect( screen.getByText( 'Local processing · no file uploads declared · no telemetry declared' ) ).toBeInTheDocument(); const sourceLinks = screen.getAllByRole('link', { name: 'Source and license', }); expect(sourceLinks).toHaveLength(2); expect( sourceLinks.find( (link) => link.getAttribute('href') === 'https://git.add-ideas.de/zemion/pdf-tools' ) ).toHaveAttribute('rel', 'noopener noreferrer'); expect( sourceLinks.find((link) => link.getAttribute('href')?.endsWith('/toolbox-portal/src/tag/v0.1.0') ) ).toBeDefined(); const launch = screen.getByRole('link', { name: /open tool/i }); expect( new URL(launch.getAttribute('href')!).searchParams.get('toolbox') ).toContain('toolbox.catalog.json'); await user.click(screen.getByRole('button', { name: 'Pin PDF Workbench' })); await waitFor(() => expect(localStorage.getItem(PREFERENCES_KEY)).toContain( 'de.add-ideas.pdf-tools' ) ); expect(screen.getByText('Pinned')).toBeInTheDocument(); await user.click( screen.getByRole('button', { name: 'Hide PDF Workbench' }) ); expect( screen.queryByRole('heading', { name: 'PDF Workbench' }) ).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('checkbox', { name: /show hidden/i })); expect( screen.getByRole('heading', { name: 'PDF Workbench' }) ).toBeInTheDocument(); }); it('shows a recoverable catalogue error', async () => { const fetchMock = vi .fn() .mockResolvedValueOnce(new Response('no', { status: 503 })) .mockImplementation(catalogueFetch()); vi.stubGlobal('fetch', fetchMock); render(); expect( await screen.findByRole('heading', { name: 'Toolbox unavailable' }) ).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Try again' })); expect( await screen.findByRole('heading', { name: 'PDF Workbench' }) ).toBeInTheDocument(); }); it('shows a dedicated empty state for a valid catalogue', async () => { vi.stubGlobal( 'fetch', vi.fn( catalogueFetch({ '/toolbox.catalog.json': Response.json({ ...catalogue, apps: [] }), }) ) ); render(); expect( await screen.findByRole('heading', { name: 'No tools yet' }) ).toBeInTheDocument(); }); it('contains preference focus and restores it to Personalize', async () => { vi.stubGlobal('fetch', vi.fn(catalogueFetch())); const user = userEvent.setup(); const { container } = render(); const personalize = screen.getByRole('button', { name: 'Personalize' }); await user.click(personalize); const dialog = screen.getByRole('dialog', { name: 'Personalize your toolbox', }); const close = screen.getByRole('button', { name: 'Close preferences' }); expect(close).toHaveFocus(); expect(container.querySelector('.site-shell')).toHaveAttribute('inert'); expect(dialog).toBeInTheDocument(); expect(screen.getByText(/does not transmit them/i)).toHaveTextContent( /Trusted code in another Toolbox app on this site can access same-origin storage/i ); const reset = screen.getByRole('button', { name: 'Reset' }); reset.focus(); await user.tab(); expect(close).toHaveFocus(); await user.keyboard('{Escape}'); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(personalize).toHaveFocus(); }); });