import { test, expect, Page } from '@playwright/test' test.describe('Frontend', () => { let page: Page test.beforeAll(async ({ browser }) => { const context = await browser.newContext() page = await context.newPage() }) test('can load homepage', async () => { await page.goto('http://localhost:3000') await expect(page).toHaveTitle(/Payload Website Template/) const heading = page.locator('h1').first() await expect(heading).toHaveText('Payload Website Template') }) }) test.describe('Participation process timeline', () => { for (const viewport of [ { width: 1440, height: 900 }, { width: 1024, height: 768 }, { width: 768, height: 1024 }, { width: 390, height: 844 }, ]) { test(`keeps all steps reachable at ${viewport.width}px`, async ({ page }) => { await page.setViewportSize(viewport) await page.goto('http://localhost:3000/teilnahme#schritte') const timeline = page.locator('#schritte') const eligibility = page.locator('#voraussetzungen') const steps = timeline.locator('[data-testid^="timeline-step-"]') await expect(timeline).toBeVisible() if (viewport.width < 768) { await expect(timeline.locator('h3')).toHaveCount(4) } else { await expect(steps).toHaveCount(4) const nextButton = timeline.getByRole('button', { name: 'Nächster Schritt', exact: true }) const visibleTrackRatio = async (index: number) => steps.nth(index).evaluate((card) => { const track = card.closest('[data-testid="timeline-track"]') if (!track) return 0 const cardRect = card.getBoundingClientRect() const trackRect = track.getBoundingClientRect() const visibleWidth = Math.max( 0, Math.min(cardRect.right, trackRect.right) - Math.max(cardRect.left, trackRect.left), ) return visibleWidth / cardRect.width }) for (let index = 0; index < 4; index += 1) { await expect.poll(() => visibleTrackRatio(index)).toBeGreaterThan(0.55) if (index < 3) { await nextButton.click() await expect(timeline.locator('[data-testid="timeline-progress-label"]')).toHaveText( `${String(index + 2).padStart(2, '0')} / 04`, ) } } const cardsOverlap = await steps.evaluateAll((cards) => cards.some((card, index) => { const nextCard = cards[index + 1] return nextCard ? card.getBoundingClientRect().right > nextCard.getBoundingClientRect().left : false })) expect(cardsOverlap).toBe(false) expect(await timeline.evaluate((element) => element.getBoundingClientRect().height)).toBeLessThan(900) } const sectionGap = await page.evaluate(() => { const timelineElement = document.querySelector('#schritte') const eligibilityElement = document.querySelector('#voraussetzungen') if (!timelineElement || !eligibilityElement) return Number.POSITIVE_INFINITY return eligibilityElement.getBoundingClientRect().top - timelineElement.getBoundingClientRect().bottom }) expect(Math.abs(sectionGap)).toBeLessThanOrEqual(2) }) } test('uses a fully readable non-animated reduced-motion interaction', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }) await page.setViewportSize({ width: 1024, height: 768 }) await page.goto('http://localhost:3000/teilnahme#schritte') const timeline = page.locator('#schritte') await timeline.getByRole('button', { name: 'Nächster Schritt', exact: true }).click() await expect(timeline.locator('[data-testid="timeline-progress-label"]')).toHaveText('02 / 04') await expect.poll(() => timeline.locator('[data-testid="timeline-step-2"]').evaluate((card) => { const track = card.closest('[data-testid="timeline-track"]') if (!track) return 0 const cardRect = card.getBoundingClientRect() const trackRect = track.getBoundingClientRect() return Math.max(0, Math.min(cardRect.right, trackRect.right) - Math.max(cardRect.left, trackRect.left)) / cardRect.width })).toBeGreaterThan(0.55) expect(await timeline.locator('[data-testid="timeline-track"]').evaluate((element) => ( getComputedStyle(element).scrollBehavior ))).toBe('auto') }) })