import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Mock next-intl — passthrough t(key) => key (Pitfall: must be before imports) vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key, })); // Mock favorites-api — all functions as controllable vi.fn() spies // (reorderFavorites hinzugefuegt, 260917-jdd — ohne diesen Eintrag // importiert das Widget undefined und der Klick auf einen Sortierpfeil // wirft TypeError.) // // 260923-lrr: die Fabrik ist jetzt ASYNCHRON und uebernimmt per // `vi.importActual` die ECHTE `FavoriteRequestError`-Klasse und // `FAVORITE_ICON_MAX_BYTES` — ein `instanceof FavoriteRequestError` im // Widget muss gegen dieselbe Klasse pruefen, die die Tests werfen, sonst // faellt `favoriteErrorKey` immer auf `favorites.error` zurueck. // `uploadFavoriteIcon`/`removeFavoriteIcon` kommen als `vi.fn()` dazu. vi.mock('@/lib/favorites-api', async () => { const actual = await vi.importActual('@/lib/favorites-api'); return { FavoriteRequestError: actual.FavoriteRequestError, FAVORITE_ICON_MAX_BYTES: actual.FAVORITE_ICON_MAX_BYTES, fetchFavorites: vi.fn(), createFavorite: vi.fn(), updateFavorite: vi.fn(), deleteFavorite: vi.fn(), reorderFavorites: vi.fn(), uploadFavoriteIcon: vi.fn(), removeFavoriteIcon: vi.fn(), }; }); // Mock dashboard-api for updateWidgetConfig (view mode persistence) vi.mock('@/lib/dashboard-api', () => ({ updateWidgetConfig: vi.fn().mockResolvedValue(undefined), })); import { FavoriteRequestError, fetchFavorites, createFavorite, updateFavorite, deleteFavorite, reorderFavorites, uploadFavoriteIcon, removeFavoriteIcon, } from '@/lib/favorites-api'; import { updateWidgetConfig } from '@/lib/dashboard-api'; // Must import AFTER mocks import { FavoritesWidget } from './favorites-widget'; // Typed mock helpers const mockFetch = fetchFavorites as ReturnType; const mockCreate = createFavorite as ReturnType; const mockUpdate = updateFavorite as ReturnType; const mockDelete = deleteFavorite as ReturnType; const mockReorder = reorderFavorites as ReturnType; const mockUploadIcon = uploadFavoriteIcon as ReturnType; const mockRemoveIcon = removeFavoriteIcon as ReturnType; const mockUpdateWidgetConfig = updateWidgetConfig as ReturnType; const BASE_FAVORITES = [ { id: 'fav-id-1', widgetId: 'fav-1', title: 'GitHub', url: 'https://github.com', iconUrl: 'https://github.com/favicon.ico', position: 0, }, { id: 'fav-id-2', widgetId: 'fav-1', title: 'Notion', url: 'https://notion.so', iconUrl: null, // null iconUrl → letter fallback 'N' position: 1, }, ]; describe('FavoritesWidget', () => { beforeEach(() => { vi.clearAllMocks(); // Default: return the base favorites list mockFetch.mockResolvedValue([...BASE_FAVORITES]); mockCreate.mockResolvedValue({ id: 'fav-id-new', widgetId: 'fav-1', title: 'New Link', url: 'https://example.com', iconUrl: null, position: 2, }); mockUpdate.mockResolvedValue({ ...BASE_FAVORITES[0], title: 'GitHub Updated', }); mockDelete.mockResolvedValue(undefined); }); afterEach(() => { vi.restoreAllMocks(); }); it('calls fetchFavorites with instanceId on mount and renders link titles', async () => { render( , ); // fetchFavorites must be called with the instanceId (widgetId scope — Pitfall 3) expect(mockFetch).toHaveBeenCalledWith('fav-1'); // After async resolution, link titles should be visible await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); expect(screen.getByText('Notion')).toBeInTheDocument(); }); }); it('shows empty message when fetchFavorites resolves to empty array', async () => { mockFetch.mockResolvedValue([]); render( , ); await waitFor(() => { expect(screen.getByText('favorites.empty')).toBeInTheDocument(); }); }); it('add: submit form in edit mode calls createFavorite with widgetId, title, url', async () => { mockFetch.mockResolvedValue([]); render( , ); await waitFor(() => { expect(screen.getByText('favorites.empty')).toBeInTheDocument(); }); // Fill in the add form (inputs are identified by placeholder translation keys) const titleInput = screen.getByPlaceholderText('favorites.addTitle'); const urlInput = screen.getByPlaceholderText('favorites.addUrl'); // Use sync fireEvent — RTL will flush state updates automatically act(() => { fireEvent.change(titleInput, { target: { value: 'New Link' } }); fireEvent.change(urlInput, { target: { value: 'https://example.com' } }); }); // Submit the form const addButton = screen.getByRole('button', { name: /favorites\.addButton/i }); act(() => { fireEvent.click(addButton); }); // waitFor polls until the async chain (createFavorite → setFavorites) completes await waitFor(() => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ widgetId: 'fav-1', title: 'New Link', url: 'https://example.com', }), ); }); // The new link title should appear in the list await waitFor(() => { expect(screen.getByText('New Link')).toBeInTheDocument(); }); }); it('edit: clicking edit, changing title, saving calls updateFavorite with id and new title', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); // Click the edit button for 'GitHub' (aria-label is the translation key) const editButtons = screen.getAllByRole('button', { name: /favorites\.editButton/i, }); act(() => { fireEvent.click(editButtons[0]); }); // Change the title in the inline edit form await waitFor(() => { expect(screen.getByDisplayValue('GitHub')).toBeInTheDocument(); }); const editTitleInput = screen.getByDisplayValue('GitHub'); act(() => { fireEvent.change(editTitleInput, { target: { value: 'GitHub Updated' } }); }); // Click Save await waitFor(() => { expect(screen.getByRole('button', { name: /favorites\.saveButton/i })).toBeInTheDocument(); }); const saveButton = screen.getByRole('button', { name: /favorites\.saveButton/i, }); act(() => { fireEvent.click(saveButton); }); // updateFavorite called with the favorite id and new title await waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith( 'fav-id-1', expect.objectContaining({ title: 'GitHub Updated' }), ); }); }); it('delete: clicking delete calls deleteFavorite with id and removes the row', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); // Click the delete button for 'GitHub' (first delete button) const deleteButtons = screen.getAllByRole('button', { name: /favorites\.deleteButton/i, }); act(() => { fireEvent.click(deleteButtons[0]); }); // deleteFavorite called with the correct id await waitFor(() => { expect(mockDelete).toHaveBeenCalledWith('fav-id-1'); }); // The deleted favorite is removed from the list await waitFor(() => { expect(screen.queryByText('GitHub')).not.toBeInTheDocument(); }); }); it('view toggle: switching to grid in edit mode renders grid container', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); // Default list view — list container should be present expect(screen.getByTestId('favorites-list')).toBeInTheDocument(); // Click the 'Kacheln' (grid) toggle button const gridButton = screen.getByRole('button', { name: /favorites\.gridView/i, }); await act(async () => { fireEvent.click(gridButton); }); // Grid container should now be rendered expect(screen.getByTestId('favorites-grid')).toBeInTheDocument(); }); it('letter fallback: link with iconUrl null renders first uppercase letter of title', async () => { render( , ); await waitFor(() => { expect(screen.getByText('Notion')).toBeInTheDocument(); }); // 'Notion' has iconUrl null — the letter fallback span should show 'N' expect(screen.getByTestId('letter-fallback-fav-id-2')).toHaveTextContent('N'); }); // quick-260916-iex: optionaler Titel describe('optionaler Titel (quick-260916-iex)', () => { it('Ansicht ohne (leeren/ungueltigen) Titel zeigt keine Kopfzeile', async () => { const r1 = render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); expect(screen.queryByRole('heading')).not.toBeInTheDocument(); expect( screen.queryByPlaceholderText('favorites.titlePlaceholder'), ).not.toBeInTheDocument(); r1.unmount(); const r2 = render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); expect(screen.queryByRole('heading')).not.toBeInTheDocument(); r2.unmount(); render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); expect(screen.queryByRole('heading')).not.toBeInTheDocument(); }); it('Ansicht mit Titel zeigt eine H2-Kopfzeile', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const heading = screen.getByRole('heading', { name: 'Werkzeuge' }); expect(heading.tagName).toBe('H2'); expect(heading.className).toContain('text-sm'); expect(heading.className).toContain('font-semibold'); expect( screen.queryByPlaceholderText('favorites.titlePlaceholder'), ).not.toBeInTheDocument(); }); it('Bearbeitungsmodus ohne Titel zeigt ein Titelfeld statt einer Kopfzeile', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const titleField = screen.getByPlaceholderText( 'favorites.titlePlaceholder', ) as HTMLInputElement; expect(titleField.value).toBe(''); expect(titleField.className).toContain('widgetNoDrag'); expect(screen.queryByRole('heading')).not.toBeInTheDocument(); expect( screen.getByRole('button', { name: /favorites\.listView/i }), ).toBeInTheDocument(); expect( screen.getByRole('button', { name: /favorites\.gridView/i }), ).toBeInTheDocument(); }); it('entprellt das Speichern des Titels um 1500 ms', async () => { vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }); try { render( , ); await act(async () => {}); const titleField = screen.getByPlaceholderText( 'favorites.titlePlaceholder', ); for (const value of ['W', 'We', 'Werkzeuge']) { await act(async () => { fireEvent.change(titleField, { target: { value } }); vi.advanceTimersByTime(200); }); } expect(mockUpdateWidgetConfig).not.toHaveBeenCalled(); await act(async () => { vi.advanceTimersByTime(1500); }); expect(mockUpdateWidgetConfig).toHaveBeenCalledTimes(1); expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('fav-1', { title: 'Werkzeuge', }); await act(async () => { fireEvent.change(titleField, { target: { value: '' } }); vi.advanceTimersByTime(1500); }); expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('fav-1', { title: '', }); } finally { vi.useRealTimers(); } }); }); // 260917-jdd: Browser-Ersatzweg (Proxy -> Direktbild -> Buchstabe) und // Sortierpfeile im Bearbeitungsmodus. describe('Ersatzbild und Sortierung (quick-260917-jdd)', () => { it('Ersatzbild bei iconUrl null: Direktbild statt Proxy-Bild, referrerpolicy no-referrer', async () => { render( , ); await waitFor(() => { expect(screen.getByText('Notion')).toBeInTheDocument(); }); const directImg = screen.getByTestId('icon-direct-fav-id-2') as HTMLImageElement; expect(directImg.src).toBe('https://notion.so/favicon.ico'); expect(directImg.getAttribute('referrerpolicy')).toBe('no-referrer'); expect(screen.queryByTestId('icon-proxy-fav-id-2')).not.toBeInTheDocument(); expect(screen.getByTestId('letter-fallback-fav-id-2')).toHaveTextContent('N'); }); it('Kette Proxy -> direkt -> Buchstabe', async () => { render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const proxyImg = screen.getByTestId('icon-proxy-fav-id-1') as HTMLImageElement; // 260923-lrr: die Proxy-Adresse traegt jetzt ?v= (Cache-Bust). expect(proxyImg.getAttribute('src')).toBe('/api-proxy/favorites/fav-id-1/icon?v=0'); expect(screen.queryByTestId('icon-direct-fav-id-1')).not.toBeInTheDocument(); act(() => { fireEvent.error(proxyImg); }); expect(screen.queryByTestId('icon-proxy-fav-id-1')).not.toBeInTheDocument(); const directImg = screen.getByTestId('icon-direct-fav-id-1') as HTMLImageElement; expect(directImg.src).toBe('https://github.com/favicon.ico'); act(() => { fireEvent.error(directImg); }); expect(screen.queryByTestId('icon-proxy-fav-id-1')).not.toBeInTheDocument(); expect(screen.queryByTestId('icon-direct-fav-id-1')).not.toBeInTheDocument(); expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G'); }); it('ausdrueckliche Symbol-Adresse (260929-lh3): Proxy -> Browser laedt iconUrl direkt (no-referrer) -> Origin-Favicon -> Buchstabe', async () => { mockFetch.mockResolvedValue([ { id: 'fav-id-9', widgetId: 'fav-1', title: 'Docuvita', url: 'https://docuvita.ctl.local/server/services/web/', iconUrl: 'https://docuvita.ctl.local/webclient/docuvita/resources/brandimage/favicon.ico', position: 0, }, ]); render(); await waitFor(() => { expect(screen.getByText('Docuvita')).toBeInTheDocument(); }); act(() => { fireEvent.error(screen.getByTestId('icon-proxy-fav-id-9')); }); const direct1 = screen.getByTestId('icon-direct-fav-id-9') as HTMLImageElement; expect(direct1.src).toBe( 'https://docuvita.ctl.local/webclient/docuvita/resources/brandimage/favicon.ico', ); expect(direct1.getAttribute('referrerpolicy')).toBe('no-referrer'); act(() => { fireEvent.error(direct1); }); const direct2 = screen.getByTestId('icon-direct-fav-id-9') as HTMLImageElement; expect(direct2.src).toBe('https://docuvita.ctl.local/favicon.ico'); act(() => { fireEvent.error(direct2); }); expect(screen.queryByTestId('icon-direct-fav-id-9')).not.toBeInTheDocument(); expect(screen.getByTestId('letter-fallback-fav-id-9')).toHaveTextContent('D'); }); it('iconUrl mit Nicht-http-Schema wird NIE direkt geladen (javascript:/data:)', async () => { mockFetch.mockResolvedValue([ { id: 'fav-id-8', widgetId: 'fav-1', title: 'Boese', url: 'https://boese.example', iconUrl: 'javascript:alert(1)', position: 0, }, ]); render(); await waitFor(() => { expect(screen.getByText('Boese')).toBeInTheDocument(); }); act(() => { fireEvent.error(screen.getByTestId('icon-proxy-fav-id-8')); }); const direct = screen.getByTestId('icon-direct-fav-id-8') as HTMLImageElement; expect(direct.src).toBe('https://boese.example/favicon.ico'); }); it('kein Direktbild bei Nicht-http-URL', async () => { mockFetch.mockResolvedValue([ { id: 'fav-id-3', widgetId: 'fav-1', title: 'Ablage', url: 'ftp://files.example', iconUrl: null, position: 0 }, ]); render( , ); await waitFor(() => { expect(screen.getByText('Ablage')).toBeInTheDocument(); }); expect(screen.queryByTestId('icon-direct-fav-id-3')).not.toBeInTheDocument(); expect(screen.queryByTestId('icon-proxy-fav-id-3')).not.toBeInTheDocument(); expect(screen.getByTestId('letter-fallback-fav-id-3')).toHaveTextContent('A'); }); it('Pfeile: Zustand (erster/letzter deaktiviert) und Klick loest reorderFavorites aus', async () => { mockReorder.mockResolvedValue([ { ...BASE_FAVORITES[1], position: 0 }, { ...BASE_FAVORITES[0], position: 1 }, ]); render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const up = screen.getAllByRole('button', { name: 'favorites.moveUpButton' }); const down = screen.getAllByRole('button', { name: 'favorites.moveDownButton' }); expect(up[0]).toBeDisabled(); expect(down[0]).not.toBeDisabled(); expect(up[1]).not.toBeDisabled(); expect(down[1]).toBeDisabled(); act(() => { fireEvent.click(down[0]); }); await waitFor(() => { expect(mockReorder).toHaveBeenCalledWith('fav-1', ['fav-id-2', 'fav-id-1']); }); await waitFor(() => { const titles = within(screen.getByTestId('favorites-list')) .getAllByRole('link') .map((a) => a.querySelector('.truncate')?.textContent); expect(titles).toEqual(['Notion', 'GitHub']); }); }); it('Fehler beim Sortieren -> Neuladen, Fehlermeldung, alte Reihenfolge', async () => { mockReorder.mockRejectedValue(new Error('boom')); render( , ); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const down = screen.getAllByRole('button', { name: 'favorites.moveDownButton' }); act(() => { fireEvent.click(down[0]); }); await waitFor(() => { expect(mockFetch).toHaveBeenCalledTimes(2); }); await waitFor(() => { expect(screen.getByText('favorites.error')).toBeInTheDocument(); }); await waitFor(() => { const titles = within(screen.getByTestId('favorites-list')) .getAllByRole('link') .map((a) => a.querySelector('.truncate')?.textContent); expect(titles).toEqual(['GitHub', 'Notion']); }); }); }); it('quick-260921-gof (Befund 14): erneutes Zeichnen mit gleicher instanceId loest KEINEN weiteren Abruf aus — die Uebersetzungs-Attrappe liefert bei jedem Durchlauf eine frische Funktion, eine Rueckkehr von `t` in die Abhaengigkeitsliste faellt hier sofort auf', async () => { const { rerender } = render( , ); await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); rerender( , ); await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); }); // 260923-lrr: eigenes Symbol hochladen, Zwischenspeicher nach Aenderung // erneuern (versionierte Symbol-Adresse, Datei-Auswahl beim Bearbeiten // und Hinzufuegen, Entfernen-Knopf, Meldungen im Formular). describe('Eigenes Symbol (quick-260923-lrr)', () => { function openEditForGitHub() { const editButtons = screen.getAllByRole('button', { name: /favorites\.editButton/i }); act(() => { fireEvent.click(editButtons[0]); }); } it('Proxy-Bild traegt ?v=; Zeile ohne iconVersion -> ?v=0', async () => { mockFetch.mockResolvedValue([{ ...BASE_FAVORITES[0], iconVersion: 3 }, BASE_FAVORITES[1]]); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); const proxyImg = screen.getByTestId('icon-proxy-fav-id-1') as HTMLImageElement; expect(proxyImg.getAttribute('src')).toBe('/api-proxy/favorites/fav-id-1/icon?v=3'); }); it('Zeile nur mit uploadedIconMime (iconUrl null) -> Proxy-Bild statt Direktbild', async () => { mockFetch.mockResolvedValue([ { id: 'fav-id-4', widgetId: 'fav-1', title: 'Eigenes', url: 'https://eigenes.invalid', iconUrl: null, uploadedIconMime: 'image/png', iconVersion: 1, position: 0, }, ]); render(); await waitFor(() => { expect(screen.getByText('Eigenes')).toBeInTheDocument(); }); expect(screen.getByTestId('icon-proxy-fav-id-4')).toBeInTheDocument(); expect(screen.queryByTestId('icon-direct-fav-id-4')).not.toBeInTheDocument(); }); it('Logo-Adress-Feld zeigt den Platzhalter favorites.iconUrlPlaceholder (kein fest verdrahteter Text mehr)', async () => { render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); expect(screen.getByPlaceholderText('favorites.iconUrlPlaceholder')).toBeInTheDocument(); }); it('Speichern mit neuer Logo-Adresse, updateFavorite liefert iconVersion 1 -> src des Proxy-Bildes endet danach auf ?v=1', async () => { mockUpdate.mockResolvedValue({ ...BASE_FAVORITES[0], iconUrl: 'https://neu.invalid/icon.png', iconVersion: 1, }); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); const iconUrlField = screen.getByPlaceholderText('favorites.iconUrlPlaceholder'); act(() => { fireEvent.change(iconUrlField, { target: { value: 'https://neu.invalid/icon.png' } }); }); const saveButton = screen.getByRole('button', { name: /favorites\.saveButton/i }); act(() => { fireEvent.click(saveButton); }); await waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith( 'fav-id-1', expect.objectContaining({ iconUrl: 'https://neu.invalid/icon.png' }), ); }); await waitFor(() => { const proxyImg = screen.getByTestId('icon-proxy-fav-id-1') as HTMLImageElement; expect(proxyImg.getAttribute('src')).toBe('/api-proxy/favorites/fav-id-1/icon?v=1'); }); }); it('Datei im Bearbeitungsformular waehlen, Speichern -> updateFavorite, danach uploadFavoriteIcon(\'fav-id-1\', Datei); Formular schliesst', async () => { mockUpdate.mockResolvedValue({ ...BASE_FAVORITES[0], iconVersion: 1 }); mockUploadIcon.mockResolvedValue({ ...BASE_FAVORITES[0], uploadedIconMime: 'image/png', iconVersion: 2, }); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); const fileField = screen.getByTestId('favorite-icon-upload-fav-id-1'); const file = new File([new Uint8Array([1, 2, 3])], 'icon.png', { type: 'image/png' }); act(() => { fireEvent.change(fileField, { target: { files: [file] } }); }); const saveButton = screen.getByRole('button', { name: /favorites\.saveButton/i }); act(() => { fireEvent.click(saveButton); }); await waitFor(() => { expect(mockUpdate).toHaveBeenCalledWith('fav-id-1', expect.objectContaining({ title: 'GitHub' })); }); await waitFor(() => { expect(mockUploadIcon).toHaveBeenCalledWith('fav-id-1', file); }); await waitFor(() => { expect(screen.queryByTestId('favorite-icon-upload-fav-id-1')).not.toBeInTheDocument(); }); }); it('updateFavorite wirft (z. B. 400 ungueltige Adresse) -> allgemeine Meldung favorites.error im Formular (role alert), Formular bleibt offen, uploadFavoriteIcon NICHT aufgerufen', async () => { mockUpdate.mockRejectedValue(new Error('Failed to update favorite')); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); const fileField = screen.getByTestId('favorite-icon-upload-fav-id-1'); const file = new File([new Uint8Array([1])], 'icon.png', { type: 'image/png' }); act(() => { fireEvent.change(fileField, { target: { files: [file] } }); }); const saveButton = screen.getByRole('button', { name: /favorites\.saveButton/i }); act(() => { fireEvent.click(saveButton); }); await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('favorites.error'); }); expect(mockUploadIcon).not.toHaveBeenCalled(); expect(screen.getByTestId('favorite-icon-upload-fav-id-1')).toBeInTheDocument(); }); it('Upload wirft FavoriteRequestError(iconTooLarge) -> favorites.iconTooLarge im Formular, Formular bleibt offen', async () => { mockUpdate.mockResolvedValue({ ...BASE_FAVORITES[0] }); mockUploadIcon.mockRejectedValue(new FavoriteRequestError('iconTooLarge')); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); const fileField = screen.getByTestId('favorite-icon-upload-fav-id-1'); const file = new File([new Uint8Array([1])], 'icon.png', { type: 'image/png' }); act(() => { fireEvent.change(fileField, { target: { files: [file] } }); }); const saveButton = screen.getByRole('button', { name: /favorites\.saveButton/i }); act(() => { fireEvent.click(saveButton); }); await waitFor(() => { expect(screen.getByRole('alert')).toHaveTextContent('favorites.iconTooLarge'); }); expect(screen.getByTestId('favorite-icon-upload-fav-id-1')).toBeInTheDocument(); }); it('Knopf favorites.iconRemoveButton nur bei gesetztem uploadedIconMime; Klick -> removeFavoriteIcon(\'fav-id-1\'), danach verschwindet der Knopf, Formular bleibt offen', async () => { mockFetch.mockResolvedValue([ { ...BASE_FAVORITES[0], uploadedIconMime: 'image/png', iconVersion: 1 }, BASE_FAVORITES[1], ]); mockRemoveIcon.mockResolvedValue({ ...BASE_FAVORITES[0], uploadedIconMime: null, iconVersion: 2 }); render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); const removeButton = screen.getByRole('button', { name: 'favorites.iconRemoveButton' }); act(() => { fireEvent.click(removeButton); }); await waitFor(() => { expect(mockRemoveIcon).toHaveBeenCalledWith('fav-id-1'); }); await waitFor(() => { expect(screen.queryByRole('button', { name: 'favorites.iconRemoveButton' })).not.toBeInTheDocument(); }); expect(screen.getByTestId('favorite-icon-upload-fav-id-1')).toBeInTheDocument(); }); it('kein Entfernen-Knopf ohne hochgeladenes Symbol', async () => { render(); await waitFor(() => { expect(screen.getByText('GitHub')).toBeInTheDocument(); }); openEditForGitHub(); expect(screen.queryByRole('button', { name: 'favorites.iconRemoveButton' })).not.toBeInTheDocument(); }); it('Hinzufuegen mit gewaehlter Datei -> createFavorite, danach uploadFavoriteIcon(created.id, Datei)', async () => { mockFetch.mockResolvedValue([]); mockUploadIcon.mockResolvedValue({ id: 'fav-id-new', widgetId: 'fav-1', title: 'New Link', url: 'https://example.com', iconUrl: null, uploadedIconMime: 'image/png', iconVersion: 1, position: 2, }); render(); await waitFor(() => { expect(screen.getByText('favorites.empty')).toBeInTheDocument(); }); const titleInput = screen.getByPlaceholderText('favorites.addTitle'); const urlInput = screen.getByPlaceholderText('favorites.addUrl'); const fileInput = screen.getByTestId('favorite-add-icon-upload'); const file = new File([new Uint8Array([1, 2, 3])], 'icon.png', { type: 'image/png' }); act(() => { fireEvent.change(titleInput, { target: { value: 'New Link' } }); fireEvent.change(urlInput, { target: { value: 'https://example.com' } }); fireEvent.change(fileInput, { target: { files: [file] } }); }); const addButton = screen.getByRole('button', { name: /favorites\.addButton/i }); act(() => { fireEvent.click(addButton); }); await waitFor(() => { expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ widgetId: 'fav-1', title: 'New Link', url: 'https://example.com' }), ); }); await waitFor(() => { expect(mockUploadIcon).toHaveBeenCalledWith('fav-id-new', file); }); await waitFor(() => { expect(screen.getByText('New Link')).toBeInTheDocument(); }); }); }); });