import { expect, test, type Page } from '@playwright/test'; const routes = ['/fundus', '/sendungen', '/eingang', '/archiv', '/verwaltung', '/live']; const viewports = [ { width: 390, height: 844, theme: 'light' }, { width: 390, height: 844, theme: 'dark' }, { width: 768, height: 900, theme: 'light' }, { width: 768, height: 900, theme: 'dark' }, { width: 1440, height: 1000, theme: 'dark' }, { width: 1440, height: 1000, theme: 'light' } ] as const; async function expectNoHorizontalOverflow(page: Page) { const dimensions = await page.evaluate(() => ({ scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth })); expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); } async function setTheme(page: Page, theme: 'light' | 'dark') { await page.evaluate((value) => localStorage.setItem('sendewerk-theme', value), theme); } test('desktop fundus is a compact, filterable list with persistent themes', async ({ page }, testInfo) => { await page.setViewportSize({ width: 1440, height: 1000 }); await page.goto('/fundus'); await expect(page.getByRole('heading', { name: 'Fundus', exact: true })).toBeVisible(); await expect(page.locator('.sidebar')).toBeVisible(); await expect(page.locator('.mobile-navigation')).toBeHidden(); await expect(page.locator('.utility-navigation a')).toHaveText('Verwaltung'); await expect(page.locator('.piece-card')).toHaveCount(4); const firstRow = await page.locator('.piece-card').nth(0).boundingBox(); const secondRow = await page.locator('.piece-card').nth(1).boundingBox(); expect(firstRow?.x).toBe(secondRow?.x); expect(firstRow?.y).toBeLessThan(secondRow?.y ?? 0); expect(firstRow?.height ?? 999).toBeLessThan(130); const searchBox = await page.locator('.search-field').boundingBox(); const filtersBox = await page.locator('.filter-tabs').boundingBox(); expect(searchBox?.height).toBe(filtersBox?.height); await expect(page.getByRole('button', { name: 'In Arbeit' })).toHaveCSS('white-space', 'nowrap'); const notice = page.locator('.preview-notice'); await expect(page.getByText('Vorschau · Fundus', { exact: true })).toBeVisible(); await expect(notice.getByText('Diese Ansicht ist schreibgeschützt.')).toBeHidden(); await notice.locator('summary').focus(); await expect(notice.locator('summary')).toBeFocused(); await page.keyboard.press('Enter'); await expect(notice.getByText('Diese Ansicht ist schreibgeschützt.')).toBeVisible(); await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('Pixelart'); await expect(page.locator('.piece-card')).toHaveCount(1); await page.getByRole('button', { name: 'In Arbeit' }).click(); await expect(page.getByRole('heading', { name: 'Kein passendes Stück' })).toBeVisible(); await page.getByRole('button', { name: 'Filter zurücksetzen' }).click(); await expect(page.locator('.piece-card')).toHaveCount(4); await page.getByRole('button', { name: 'Darstellung: System' }).click(); await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); await page.reload(); await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); await expect(page.getByRole('button', { name: 'Darstellung: Hell' })).toBeVisible(); await expectNoHorizontalOverflow(page); await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true }); }); test('desktop builder keeps the fundus beside the wider run sheet', async ({ page }, testInfo) => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto('/fundus'); await setTheme(page, 'dark'); await page.goto('/sendungen'); await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); await expect(page.getByRole('heading', { name: 'Sendungen', exact: true })).toBeVisible(); await expect(page.getByText('Herbstsendung: Kleine Welten', { exact: true })).toBeVisible(); await expect(page.locator('.source-pane')).toBeVisible(); await expect(page.locator('.run-pane')).toBeVisible(); await expect(page.locator('.mobile-pane-switch')).toBeHidden(); await expect( page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' }) ).toBeVisible(); const sourceBox = await page.locator('.source-pane').boundingBox(); const runBox = await page.locator('.run-pane').boundingBox(); expect(sourceBox?.x).toBeLessThan(runBox?.x ?? 0); expect(sourceBox?.width ?? 9999).toBeLessThan(runBox?.width ?? 0); await expectNoHorizontalOverflow(page); await page.screenshot({ path: testInfo.outputPath('sendungen-desktop-dark.png'), fullPage: true }); }); test('narrow desktop retains the sidebar and switches the builder panes', async ({ page }) => { await page.setViewportSize({ width: 960, height: 850 }); await page.goto('/sendungen'); await expect(page.locator('.sidebar')).toBeVisible(); await expect(page.locator('.mobile-navigation')).toBeHidden(); await expect(page.locator('.mobile-pane-switch')).toBeVisible(); await expect(page.locator('.run-pane')).toBeVisible(); await expect(page.locator('.source-pane')).toBeHidden(); await page.getByRole('button', { name: 'Fundus', exact: true }).click(); await expect(page.locator('.source-pane')).toBeVisible(); await expect(page.locator('.run-pane')).toBeHidden(); await expectNoHorizontalOverflow(page); }); test('mobile and tablet navigation do not obscure switchable content', async ({ page }, testInfo) => { for (const width of [390, 768]) { await page.setViewportSize({ width, height: 900 }); await page.goto('/sendungen'); await expect(page.locator('.sidebar')).toBeHidden(); await expect(page.locator('.mobile-navigation')).toBeVisible(); await expect(page.locator('.run-pane')).toBeVisible(); await page.getByRole('button', { name: 'Fundus', exact: true }).click(); await expect(page.locator('.source-pane')).toBeVisible(); await expect(page.locator('.run-pane')).toBeHidden(); await expectNoHorizontalOverflow(page); await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); const verticalSpace = await page.evaluate(() => { const navigation = document.querySelector('.mobile-navigation')?.getBoundingClientRect(); const content = document.querySelector('.source-pane')?.getBoundingClientRect(); return { contentBottom: content?.bottom ?? 0, navigationTop: navigation?.top ?? 0 }; }); expect(verticalSpace.contentBottom).toBeLessThanOrEqual(verticalSpace.navigationTop + 1); } await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/sendungen'); await page.screenshot({ path: testInfo.outputPath('sendungen-mobile.png'), fullPage: true }); }); test('all work areas fit at required widths in light and dark mode', async ({ page }, testInfo) => { for (const viewport of viewports) { await page.setViewportSize({ width: viewport.width, height: viewport.height }); await page.goto('/fundus'); await setTheme(page, viewport.theme); for (const route of routes) { await page.goto(route); await expect(page.locator('html')).toHaveAttribute('data-theme', viewport.theme); await expectNoHorizontalOverflow(page); } } await page.setViewportSize({ width: 768, height: 900 }); await page.goto('/fundus'); await setTheme(page, 'dark'); await page.goto('/verwaltung'); await page.screenshot({ path: testInfo.outputPath('verwaltung-tablet-dark.png'), fullPage: true }); }); test('long titles and longer fundus lists reflow without overflow', async ({ page }, testInfo) => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto('/fundus'); await expect(page.locator('.piece-card')).toHaveCount(4); await page.evaluate(() => { const list = document.querySelector('.piece-grid'); const rows = Array.from(document.querySelectorAll('.piece-card')); const title = rows[0]?.querySelector('h2'); if (title) { title.textContent = 'Ein außergewöhnlich langer redaktioneller Titel über kleine Konsolen, große Ideen und unerwartete Wendungen'; } for (let repetition = 0; repetition < 3; repetition += 1) { for (const row of rows) list?.append(row.cloneNode(true)); } }); await expect(page.locator('.piece-card')).toHaveCount(16); await expectNoHorizontalOverflow(page); await page.setViewportSize({ width: 390, height: 844 }); await expectNoHorizontalOverflow(page); await expect(page.locator('.piece-card').first()).toHaveCSS('flex-direction', 'column'); const mobileCard = await page.locator('.piece-card').first().boundingBox(); const mobilePrimary = await page.locator('.piece-primary').first().boundingBox(); expect(mobilePrimary?.width ?? 0).toBeGreaterThan((mobileCard?.width ?? 0) - 40); await expect(page.locator('.piece-card').first().getByRole('heading')).toContainText( 'Ein außergewöhnlich langer redaktioneller Titel' ); await page.screenshot({ path: testInfo.outputPath('fundus-mobile-long-content.png'), fullPage: true }); }); test('empty states and administration stay concise', async ({ page }) => { for (const width of [390, 1280]) { await page.setViewportSize({ width, height: 900 }); for (const route of ['/eingang', '/archiv']) { await page.goto(route); const emptyState = page.locator('.planned-panel'); await expect(emptyState).toBeVisible(); const box = await emptyState.boundingBox(); expect(box?.height ?? 999).toBeLessThan(150); await expectNoHorizontalOverflow(page); } } await page.setViewportSize({ width: 1280, height: 900 }); await page.goto('/verwaltung'); const panels = page.locator('.taxonomy-panel'); await expect(panels).toHaveCount(3); const boxes = await Promise.all([ panels.nth(0).boundingBox(), panels.nth(1).boundingBox(), panels.nth(2).boundingBox() ]); expect(boxes[0]?.y).toBe(boxes[1]?.y); expect(boxes[1]?.y).toBe(boxes[2]?.y); }); test('live preview retains its existing readable controls', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await page.goto('/live'); await expect(page.getByText('Vorschau · Live-Modus', { exact: true })).toBeVisible(); await expect(page.getByText('Eine Kurbel, ein gelber Knopf und sehr viel Charme')).toBeVisible(); const before = await page .locator('.live-current') .evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size')); await page.getByRole('button', { name: 'Schrift vergrößern' }).click(); const after = await page .locator('.live-current') .evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size')); expect(after).not.toBe(before); await page.getByText('Recherche öffnen').click(); await expect(page.locator('.research-details')).toHaveAttribute('open', ''); await expectNoHorizontalOverflow(page); });