- + + {piece.title} + {piece.summary} + + {piece.category} + + {piece.format} + {#each piece.topics as topic (topic)}{topic}{/each} + + + {statusLabel[piece.status]} + + {formatClock(piece.durationSeconds)} geplant + {piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'} + - {piece.title} - {piece.summary} - - {piece.format} - {#each piece.topics as topic (topic)}{topic}{/each} - - {/each} diff --git a/src/routes/sendungen/+page.svelte b/src/routes/sendungen/+page.svelte index 33cc5d3..c8394fd 100644 --- a/src/routes/sendungen/+page.svelte +++ b/src/routes/sendungen/+page.svelte @@ -18,9 +18,10 @@ - - Nächste Sendung · {data.show.startsAtBerlin} Uhr - {data.show.title} + + Sendungen + {data.show.title} + {data.show.startsAtBerlin} Uhr {plannedCount}Stücke @@ -57,15 +58,6 @@ {data.pieces.filter((piece) => piece.status === 'ready').length} - - ⌕Fundus durchsuchen - - {#each data.pieces.filter((piece) => piece.status === 'ready') as piece (piece.id)} @@ -116,10 +108,9 @@ {#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)} - ⠿ {usage.title}{formatClock(usage.durationSeconds)} · Eigene Sendungskopie{formatClock(usage.durationSeconds)} · Stand für diese Sendung {:else} - - Eigenständiger Spieleblock · keine Piece-Kopie nötig - + Spieleblock ohne vorbereiteten Fundusinhalt {/if} {/each} diff --git a/src/routes/verwaltung/+page.svelte b/src/routes/verwaltung/+page.svelte index 8f0195c..2a33f1d 100644 --- a/src/routes/verwaltung/+page.svelte +++ b/src/routes/verwaltung/+page.svelte @@ -7,12 +7,8 @@ - System & Struktur Verwaltung - - Die bereits angelegte Taxonomie ist sichtbar; Bearbeitung und Benutzerverwaltung folgen mit - serverseitigen Rechten. - + Abschnittskategorien, Formate und Themen im Überblick. @@ -38,13 +34,6 @@ {#each data.taxonomy.topics as topic (topic.id)}{topic.name}{/each} - - Etappe 2 - Konten & Rollen - - Barkeeper, Thekencrew und technische Zugänge werden erst mit vollständiger Authentifizierung - verwaltbar. - - + Konten und Rollen sind noch nicht verfügbar. diff --git a/tests/ui.e2e.ts b/tests/ui.e2e.ts index 867ca7b..e2f2f4a 100644 --- a/tests/ui.e2e.ts +++ b/tests/ui.e2e.ts @@ -1,77 +1,232 @@ -import { expect, test } from '@playwright/test'; +import { expect, test, type Page } from '@playwright/test'; -test('desktop fundus is usable, filterable and keeps its theme', async ({ page }, testInfo) => { +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: 'Gute Stücke, sofort griffbereit.' }) - ).toBeVisible(); - await expect(page.getByText('Gestaltungsvorschau: Fundus')).toBeVisible(); + 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 expect( - page.getByRole('heading', { name: 'Pixelart, die nicht nostalgisch sein will' }) - ).toBeVisible(); + 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(); - - const dimensions = await page.evaluate(() => ({ - scrollWidth: document.documentElement.scrollWidth, - clientWidth: document.documentElement.clientWidth - })); - expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); - await page.screenshot({ path: testInfo.outputPath('fundus-desktop.png'), fullPage: true }); + await expectNoHorizontalOverflow(page); + await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true }); }); -test('desktop builder shows fundus and run side by side', async ({ page }) => { - await page.setViewportSize({ width: 1440, height: 1000 }); +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.getByText('Gestaltungsvorschau: Sendungsbau')).toBeVisible(); + 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.getByRole('heading', { name: 'Herbstsendung: Kleine Welten' })).toBeVisible(); + await expect(page.locator('.mobile-pane-switch')).toBeHidden(); await expect( page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' }) ).toBeVisible(); - const boxes = await Promise.all([ - page.locator('.source-pane').boundingBox(), - page.locator('.run-pane').boundingBox() - ]); - expect(boxes[0]?.x).toBeLessThan(boxes[1]?.x ?? 0); + 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('mobile builder switches panes without horizontal scrolling', async ({ page }, testInfo) => { - await page.setViewportSize({ width: 390, height: 844 }); +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); +}); - const dimensions = await page.evaluate(() => ({ - scrollWidth: document.documentElement.scrollWidth, - clientWidth: document.documentElement.clientWidth - })); - expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); +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('live preview exposes readable prompts and working text controls', async ({ page }) => { +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('Gestaltungsvorschau: Live-Modus')).toBeVisible(); + 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') @@ -83,4 +238,5 @@ test('live preview exposes readable prompts and working text controls', async ({ expect(after).not.toBe(before); await page.getByText('Recherche öffnen').click(); await expect(page.locator('.research-details')).toHaveAttribute('open', ''); + await expectNoHorizontalOverflow(page); });
@@ -116,10 +108,9 @@ {#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)} - ⠿ {usage.title}{formatClock(usage.durationSeconds)} · Eigene Sendungskopie{formatClock(usage.durationSeconds)} · Stand für diese Sendung {:else} - - Eigenständiger Spieleblock · keine Piece-Kopie nötig - + Spieleblock ohne vorbereiteten Fundusinhalt {/if} {/each} diff --git a/src/routes/verwaltung/+page.svelte b/src/routes/verwaltung/+page.svelte index 8f0195c..2a33f1d 100644 --- a/src/routes/verwaltung/+page.svelte +++ b/src/routes/verwaltung/+page.svelte @@ -7,12 +7,8 @@ - System & Struktur Verwaltung - - Die bereits angelegte Taxonomie ist sichtbar; Bearbeitung und Benutzerverwaltung folgen mit - serverseitigen Rechten. - + Abschnittskategorien, Formate und Themen im Überblick. @@ -38,13 +34,6 @@ {#each data.taxonomy.topics as topic (topic.id)}{topic.name}{/each} - - Etappe 2 - Konten & Rollen - - Barkeeper, Thekencrew und technische Zugänge werden erst mit vollständiger Authentifizierung - verwaltbar. - - + Konten und Rollen sind noch nicht verfügbar. diff --git a/tests/ui.e2e.ts b/tests/ui.e2e.ts index 867ca7b..e2f2f4a 100644 --- a/tests/ui.e2e.ts +++ b/tests/ui.e2e.ts @@ -1,77 +1,232 @@ -import { expect, test } from '@playwright/test'; +import { expect, test, type Page } from '@playwright/test'; -test('desktop fundus is usable, filterable and keeps its theme', async ({ page }, testInfo) => { +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: 'Gute Stücke, sofort griffbereit.' }) - ).toBeVisible(); - await expect(page.getByText('Gestaltungsvorschau: Fundus')).toBeVisible(); + 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 expect( - page.getByRole('heading', { name: 'Pixelart, die nicht nostalgisch sein will' }) - ).toBeVisible(); + 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(); - - const dimensions = await page.evaluate(() => ({ - scrollWidth: document.documentElement.scrollWidth, - clientWidth: document.documentElement.clientWidth - })); - expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); - await page.screenshot({ path: testInfo.outputPath('fundus-desktop.png'), fullPage: true }); + await expectNoHorizontalOverflow(page); + await page.screenshot({ path: testInfo.outputPath('fundus-desktop-light.png'), fullPage: true }); }); -test('desktop builder shows fundus and run side by side', async ({ page }) => { - await page.setViewportSize({ width: 1440, height: 1000 }); +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.getByText('Gestaltungsvorschau: Sendungsbau')).toBeVisible(); + 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.getByRole('heading', { name: 'Herbstsendung: Kleine Welten' })).toBeVisible(); + await expect(page.locator('.mobile-pane-switch')).toBeHidden(); await expect( page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' }) ).toBeVisible(); - const boxes = await Promise.all([ - page.locator('.source-pane').boundingBox(), - page.locator('.run-pane').boundingBox() - ]); - expect(boxes[0]?.x).toBeLessThan(boxes[1]?.x ?? 0); + 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('mobile builder switches panes without horizontal scrolling', async ({ page }, testInfo) => { - await page.setViewportSize({ width: 390, height: 844 }); +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); +}); - const dimensions = await page.evaluate(() => ({ - scrollWidth: document.documentElement.scrollWidth, - clientWidth: document.documentElement.clientWidth - })); - expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); +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('live preview exposes readable prompts and working text controls', async ({ page }) => { +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('Gestaltungsvorschau: Live-Modus')).toBeVisible(); + 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') @@ -83,4 +238,5 @@ test('live preview exposes readable prompts and working text controls', async ({ expect(after).not.toBe(before); await page.getByText('Recherche öffnen').click(); await expect(page.locator('.research-details')).toHaveAttribute('open', ''); + await expectNoHorizontalOverflow(page); });