import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // next-intl — Durchreiche t(key) => key (Muster stopwatch-widget.test.tsx) vi.mock('next-intl', () => ({ useTranslations: () => (key: string) => key, })); import { PictureFrameWidget } from './picture-frame-widget'; /** * picture-frame-widget.test — NEU (quick-260921-pi9). * * Kachel: Leerzustand, je Variante (URL direkt mit no-referrer, * Upload ueber den Proxy-Pfad), Ausschnitt-Klassen, Unterschrift-Streifen, * Wechsel per Timer inkl. Raeumung beim Aushaengen, kein Wechsel bei 0, * kaputte Bilder verlassen den Umlauf, Grossansicht (Escape / Hintergrund / * Schliessen-Knopf, Fokus-Rueckgabe, Pause des Wechsels), kein Knopf im * Bearbeitungsmodus. */ const URL_A = 'https://bilder.example/a.jpg'; const URL_B = 'https://bilder.example/b.jpg'; function renderWidget(config: Record, isEditMode = false) { return render(); } function currentImg(): HTMLImageElement { return screen.getByTestId('picture-frame-image') as HTMLImageElement; } beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { cleanup(); vi.useRealTimers(); }); describe('PictureFrameWidget (quick-260921-pi9)', () => { it('Test 1: leere Konfiguration -> Leerhinweis, kein Bild', () => { renderWidget({}); expect(screen.getByText('pictureFrame.empty')).toBeInTheDocument(); expect(screen.queryByTestId('picture-frame-image')).not.toBeInTheDocument(); }); it('Test 2: URL-Eintrag -> mit der Adresse, referrerPolicy no-referrer, object-contain, alt = Unterschrift', () => { renderWidget({ images: [{ kind: 'url', url: URL_A, caption: 'Urlaub' }] }); const img = currentImg(); expect(img.getAttribute('src')).toBe(URL_A); expect(img.getAttribute('referrerpolicy')).toBe('no-referrer'); expect(img.className).toContain('object-contain'); expect(img.getAttribute('alt')).toBe('Urlaub'); expect(screen.getByTestId('picture-frame-caption')).toHaveTextContent('Urlaub'); }); it('Test 3: fit cover -> object-cover; ohne Unterschrift alt="" und kein Streifen', () => { renderWidget({ images: [{ kind: 'url', url: URL_A }], fit: 'cover' }); const img = currentImg(); expect(img.className).toContain('object-cover'); expect(img.getAttribute('alt')).toBe(''); expect(screen.queryByTestId('picture-frame-caption')).not.toBeInTheDocument(); }); it('Test 4: Upload-Eintrag -> src ueber den Proxy-Pfad /api-proxy/dashboard/images/', () => { renderWidget({ images: [{ kind: 'upload', imageId: 'img 1' }] }); expect(currentImg().getAttribute('src')).toBe('/api-proxy/dashboard/images/img%201'); }); it('Test 5: Intervall 5 s mit zwei Bildern -> nach 5000 ms das zweite, nach 10000 ms wieder das erste; unmount raeumt den Timer', () => { const { unmount } = renderWidget({ images: [ { kind: 'url', url: URL_A }, { kind: 'url', url: URL_B }, ], intervalSeconds: 5, }); expect(currentImg().getAttribute('src')).toBe(URL_A); act(() => { vi.advanceTimersByTime(5000); }); expect(currentImg().getAttribute('src')).toBe(URL_B); act(() => { vi.advanceTimersByTime(5000); }); expect(currentImg().getAttribute('src')).toBe(URL_A); expect(vi.getTimerCount()).toBe(1); unmount(); expect(vi.getTimerCount()).toBe(0); }); it('Test 6: intervalSeconds 0 -> nie ein Wechsel, kein Timer; ein einzelnes Bild -> ebenfalls kein Timer', () => { renderWidget({ images: [ { kind: 'url', url: URL_A }, { kind: 'url', url: URL_B }, ], intervalSeconds: 0, }); expect(vi.getTimerCount()).toBe(0); act(() => { vi.advanceTimersByTime(60_000); }); expect(currentImg().getAttribute('src')).toBe(URL_A); cleanup(); renderWidget({ images: [{ kind: 'url', url: URL_A }], intervalSeconds: 5 }); expect(vi.getTimerCount()).toBe(0); }); it('Test 7: Zufall mit drei Bildern -> ueber 30 Wechsel nie dasselbe Bild zweimal hintereinander', () => { renderWidget({ images: [ { kind: 'url', url: URL_A }, { kind: 'url', url: URL_B }, { kind: 'url', url: 'https://bilder.example/c.jpg' }, ], intervalSeconds: 5, order: 'random', }); let previous = currentImg().getAttribute('src'); for (let i = 0; i < 30; i++) { act(() => { vi.advanceTimersByTime(5000); }); const next = currentImg().getAttribute('src'); expect(next).not.toBe(previous); previous = next; } }); it('Test 8: onError nimmt den Eintrag aus dem Umlauf — bei zwei Eintraegen bleibt nur der andere; sind alle kaputt, steht pictureFrame.unavailable', () => { renderWidget({ images: [ { kind: 'url', url: URL_A }, { kind: 'url', url: URL_B }, ], intervalSeconds: 5, }); fireEvent.error(currentImg()); expect(currentImg().getAttribute('src')).toBe(URL_B); // nur noch ein Bild: kein Wechsel mehr expect(vi.getTimerCount()).toBe(0); act(() => { vi.advanceTimersByTime(10_000); }); expect(currentImg().getAttribute('src')).toBe(URL_B); fireEvent.error(currentImg()); expect(screen.queryByTestId('picture-frame-image')).not.toBeInTheDocument(); expect(screen.getByText('pictureFrame.unavailable')).toBeInTheDocument(); }); it('Test 9: Klick auf das Bild oeffnet die Grossansicht (role=dialog, grosses Bild, Unterschrift); Escape schliesst, Fokus zurueck am Bild-Knopf', () => { renderWidget({ images: [{ kind: 'url', url: URL_A, caption: 'Urlaub' }] }); const openButton = screen.getByRole('button', { name: 'pictureFrame.open' }); fireEvent.click(openButton); const dialog = screen.getByRole('dialog'); expect(dialog).toBeInTheDocument(); expect(document.activeElement).toBe(dialog); expect(screen.getByTestId('picture-frame-lightbox-image').getAttribute('src')).toBe(URL_A); expect(screen.getByTestId('picture-frame-lightbox-image').getAttribute('referrerpolicy')).toBe('no-referrer'); expect(screen.getByTestId('picture-frame-lightbox-caption')).toHaveTextContent('Urlaub'); fireEvent.keyDown(document, { key: 'Escape' }); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(document.activeElement).toBe(openButton); }); it('Test 10: Hintergrund-Knopf und Schliessen-Knopf schliessen die Grossansicht', () => { renderWidget({ images: [{ kind: 'url', url: URL_A }] }); const openButton = screen.getByRole('button', { name: 'pictureFrame.open' }); fireEvent.click(openButton); fireEvent.click(screen.getByTestId('picture-frame-lightbox-backdrop')); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); fireEvent.click(openButton); fireEvent.click(screen.getByTestId('picture-frame-lightbox-close')); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(document.activeElement).toBe(openButton); }); it('Test 11: bei offener Grossansicht pausiert der Wechsel; nach dem Schliessen laeuft er weiter', () => { renderWidget({ images: [ { kind: 'url', url: URL_A }, { kind: 'url', url: URL_B }, ], intervalSeconds: 5, }); fireEvent.click(screen.getByRole('button', { name: 'pictureFrame.open' })); // Kein Timer-Zaehler hier: nach einer Interaktion haelt React selbst // kurz einen Scheduler-Timer (gemessen: 1 nach dem Oeffnen, obwohl das // Intervall geraeumt ist). Belegt wird das Verhalten — vier Intervalle // vergehen, das Bild bleibt. act(() => { vi.advanceTimersByTime(20_000); }); expect(screen.getByTestId('picture-frame-lightbox-image').getAttribute('src')).toBe(URL_A); expect(currentImg().getAttribute('src')).toBe(URL_A); fireEvent.keyDown(document, { key: 'Escape' }); act(() => { vi.advanceTimersByTime(5000); }); expect(currentImg().getAttribute('src')).toBe(URL_B); }); it('Test 12: Bearbeitungsmodus — kein Knopf im Widget, Klick auf das Bild oeffnet nichts', () => { renderWidget({ images: [{ kind: 'url', url: URL_A, caption: 'Urlaub' }] }, true); expect(screen.queryByRole('button')).not.toBeInTheDocument(); fireEvent.click(currentImg()); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); // Bild und Streifen werden trotzdem gezeigt expect(currentImg().getAttribute('src')).toBe(URL_A); expect(screen.getByTestId('picture-frame-caption')).toHaveTextContent('Urlaub'); }); });