feat: add new contact form, hero, features, and richtext blocks; implement scroll-reveal action and update styles

- Introduced ContactFormBlock, FeaturesBlock, HeroBlock, and RichtextBlock components.
- Implemented a scroll-reveal action for animations on element visibility.
- Enhanced CSS styles for better theming and prose formatting.
- Added localization support for new components and updated existing translations.
- Created e2e tests for demo pages including contact form validation and navigation.
- Added a video tour showcasing the demo pages and interactions.
This commit is contained in:
2026-02-26 03:54:07 +00:00
parent e8fd38e98a
commit 40ffa8207e
27 changed files with 2009 additions and 98 deletions

218
tests/e2e/demo.spec.ts Normal file
View File

@@ -0,0 +1,218 @@
import { test, expect, waitForSpaReady, navigateToRoute, clickSpaLink } from "./fixtures"
/**
* Helper: Force all scroll-reveal elements to be visible.
* The `.reveal` class starts with opacity:0 and only animates in
* when the IntersectionObserver fires — which doesn't happen
* reliably in headless Playwright screenshots/assertions.
*/
async function revealAll(page: import("@playwright/test").Page) {
await page.evaluate(() => document.querySelectorAll(".reveal").forEach((e) => e.classList.add("revealed")))
}
test.describe("Demo — Homepage", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/de/")
await waitForSpaReady(page)
await revealAll(page)
})
test("should render hero section with headline and CTA", async ({ page }) => {
const hero = page.locator("[data-block='hero']").first()
await expect(hero).toBeVisible()
const h1 = hero.locator("h1")
await expect(h1).toBeVisible()
await expect(h1).not.toBeEmpty()
const cta = hero.locator("a[href*='#']")
await expect(cta).toBeVisible()
})
test("should render features section with cards", async ({ page }) => {
const features = page.locator("[data-block='features']")
await expect(features).toBeVisible()
const heading = features.locator("h2")
await expect(heading).toBeVisible()
const cards = features.locator(".feature-card")
await expect(cards).toHaveCount(6)
})
test("should render richtext section with image", async ({ page }) => {
const richtext = page.locator("[data-block='richtext']").first()
await expect(richtext).toBeVisible()
const img = richtext.locator("img")
await expect(img).toBeVisible()
})
test("should render accordion with expandable items", async ({ page }) => {
const accordion = page.locator("[data-block='accordion']")
await expect(accordion).toBeVisible()
const buttons = accordion.locator("button")
const count = await buttons.count()
expect(count).toBeGreaterThanOrEqual(3)
// First item should be expanded by default
const firstButton = buttons.first()
await expect(firstButton).toHaveAttribute("aria-expanded", "true")
// Click a collapsed item to expand it
const secondButton = buttons.nth(1)
await expect(secondButton).toHaveAttribute("aria-expanded", "false")
await secondButton.click()
await expect(secondButton).toHaveAttribute("aria-expanded", "true")
})
test("should have footer with navigation and language selector", async ({ page }) => {
const footer = page.locator("footer")
await expect(footer).toBeVisible()
const navLinks = footer.locator("nav a, ul a")
const linkCount = await navLinks.count()
expect(linkCount).toBeGreaterThanOrEqual(3)
// Language links in footer
const deLangLink = footer.locator('a:has-text("Deutsch")')
const enLangLink = footer.locator('a:has-text("English")')
await expect(deLangLink).toBeVisible()
await expect(enLangLink).toBeVisible()
})
})
test.describe("Demo — About Page", () => {
test("should load about page with hero and content", async ({ page }) => {
await page.goto("/en/about")
await waitForSpaReady(page)
await revealAll(page)
const hero = page.locator("[data-block='hero']").first()
await expect(hero).toBeVisible()
const h1 = hero.locator("h1")
await expect(h1).toContainText("About")
const richtextBlocks = page.locator("[data-block='richtext']")
const count = await richtextBlocks.count()
expect(count).toBeGreaterThanOrEqual(2)
})
})
test.describe("Demo — Contact Page", () => {
test("should load contact page with form", async ({ page }) => {
await page.goto("/en/contact")
await waitForSpaReady(page)
await revealAll(page)
const hero = page.locator("[data-block='hero']").first()
await expect(hero).toBeVisible()
const form = page.locator("[data-block='contact-form']")
await expect(form).toBeVisible()
})
test("should have all form fields", async ({ page }) => {
await page.goto("/en/contact")
await waitForSpaReady(page)
await revealAll(page)
await expect(page.getByLabel("Name")).toBeVisible()
await expect(page.getByLabel("Email")).toBeVisible()
await expect(page.locator("select, [role='combobox']").first()).toBeVisible()
await expect(page.getByLabel("Message")).toBeVisible()
await expect(page.locator("button[type='submit'], button:has-text('Send')")).toBeVisible()
})
test("should validate required fields", async ({ page }) => {
await page.goto("/en/contact")
await waitForSpaReady(page)
await revealAll(page)
// Click send without filling fields
const submitBtn = page.locator("button[type='submit'], button:has-text('Send')").first()
await submitBtn.click()
// Should show validation errors (form stays, no success toast)
await expect(page.locator("[data-block='contact-form']")).toBeVisible()
})
})
test.describe("Demo — Navigation", () => {
test("should navigate between pages via header links", async ({ page }) => {
await page.goto("/en/")
await waitForSpaReady(page)
// Navigate to About
await page.locator('header nav a[href*="/about"]').click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toContain("/about")
await expect(page.locator("[data-block='hero'] h1")).toContainText("About")
// Navigate to Contact
await page.locator('header nav a[href*="/contact"]').click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toContain("/contact")
await expect(page.locator("[data-block='hero'] h1")).toContainText("Contact")
// Navigate back to Home
await page.locator('header a[href="/en"]').first().click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toMatch(/\/en\/?$/)
})
test("should switch language with route translation", async ({ page }) => {
// Start on English about page
await page.goto("/en/about")
await waitForSpaReady(page)
expect(page.url()).toContain("/en/about")
// Click German language link — should translate route to /de/ueber-uns
const deLink = page.locator('a[href*="/de/ueber-uns"]').first()
await expect(deLink).toBeVisible()
await deLink.click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toContain("/de/ueber-uns")
// Verify hero updated to German
await expect(page.locator("[data-block='hero'] h1")).toBeVisible()
})
test("should switch language on contact page with route translation", async ({ page }) => {
await page.goto("/en/contact")
await waitForSpaReady(page)
const deLink = page.locator('a[href*="/de/kontakt"]').first()
await expect(deLink).toBeVisible()
await deLink.click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toContain("/de/kontakt")
})
})
test.describe("Demo — 404 Page", () => {
test("should show 404 for unknown routes", async ({ page }) => {
await page.goto("/en/nonexistent-page")
await waitForSpaReady(page)
await expect(page.locator("text=404")).toBeVisible()
await expect(page.locator("h1")).toContainText("Page not found")
// Use the specific "Back to Home" link in the 404 main content, not header/footer
const homeLink = page.getByRole("link", { name: "Back to Home" })
await expect(homeLink).toBeVisible()
})
test("should navigate back from 404 to home", async ({ page }) => {
await page.goto("/en/nonexistent-page")
await waitForSpaReady(page)
const homeLink = page.getByRole("link", { name: "Back to Home" })
await homeLink.click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toMatch(/\/en\/?$/)
await expect(page.locator("[data-block='hero']")).toBeVisible()
})
})