feat: enhance admin API helpers with CRUD operations for collections and seed data management

- Added functions for creating, updating, deleting, and listing collection entries in admin API.
- Introduced seed data management for consistent test content across tests.
- Updated global setup and teardown processes to ensure seeded content is created and cleaned up.
- Refactored existing tests to utilize seeded content for improved reliability and maintainability.
This commit is contained in:
2026-05-12 20:36:06 +00:00
parent 491f495c66
commit 1b24bb2157
17 changed files with 697 additions and 444 deletions
+35 -170
View File
@@ -1,218 +1,83 @@
import { test, expect, waitForSpaReady, navigateToRoute, clickSpaLink } from "./fixtures"
import { test, expect, clickSpaLink, waitForSpaReady } from "./fixtures"
import { SEEDED_TEST_CONTENT } from "../fixtures/test-constants"
/**
* 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")))
await page.evaluate(() => document.querySelectorAll(".reveal").forEach((entry) => entry.classList.add("revealed")))
}
test.describe("Demo — Homepage", () => {
test.describe("Seeded Public Frontend", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/de/")
await page.goto(`/de${SEEDED_TEST_CONTENT.home.path}`)
await waitForSpaReady(page)
await revealAll(page)
})
test("should render hero section with headline and CTA", async ({ page }) => {
test("renders hero, features, richtext and accordion blocks on the seeded home page", async ({ page }) => {
const hero = page.locator("[data-block='hero']").first()
await expect(hero).toBeVisible()
await expect(hero.locator("h1")).toHaveText("Playwright Seed Startseite")
await expect(hero.locator("a").first()).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()
await expect(features.locator(".feature-card")).toHaveCount(3)
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()
await expect(richtext).toContainText("formatierter HTML-Inhalt")
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
await expect(buttons.first()).toHaveAttribute("aria-expanded", "true")
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")
test("renders the seeded contact page and validates the current form UI", async ({ page }) => {
await page.goto(`/en${SEEDED_TEST_CONTENT.contact.path}`)
await waitForSpaReady(page)
await revealAll(page)
const hero = page.locator("[data-block='hero']").first()
await expect(hero).toBeVisible()
await expect(hero.locator("h1")).toHaveText("Contact for the test run")
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()
const submitBtn = page.locator("button[type='submit'], button:has-text('Send')").first()
await expect(submitBtn).toBeVisible()
await submitBtn.click()
await expect(page.locator("[data-block='contact-form']")).toBeVisible()
})
test("should validate required fields", async ({ page }) => {
await page.goto("/en/contact")
test("shows the 404 state for inactive seeded routes and can return home", async ({ page }) => {
await page.goto(`/de${SEEDED_TEST_CONTENT.inactive.path}`)
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()
await expect(page.locator("main")).toContainText("404")
// Should show validation errors (form stays, no success toast)
const homeLink = page.locator("main").getByRole("link", { name: "Zur Startseite" })
await expect(homeLink).toBeVisible()
await homeLink.click()
await waitForSpaReady(page)
await expect(page).toHaveURL(/\/de\/?$/)
})
test("uses SPA navigation for the seeded CTA without a full reload", async ({ page }) => {
await page.goto(`/en${SEEDED_TEST_CONTENT.home.path}`)
await waitForSpaReady(page)
await clickSpaLink(page, "[data-block='hero'] a")
await expect(page).toHaveURL(new RegExp(`/en${SEEDED_TEST_CONTENT.contact.path}$`))
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()
})
})
+1 -85
View File
@@ -1,34 +1,9 @@
import { test as base, expect, type Page } from "@playwright/test"
import { ensureTestUser, type TestUserCredentials } from "../api/helpers/test-user"
import { attachConsoleMonitor } from "../fixtures/console-monitor"
const API_BASE = "/api"
/**
* Shared E2E test fixtures.
*
* Worker-scoped:
* - `testUser` persistent test user (created/reused once per worker)
*
* Test-scoped:
* - `authedPage` Page with logged-in user (token injection via sessionStorage)
* - `accessToken` raw JWT access token
*/
type E2eWorkerFixtures = {
testUser: TestUserCredentials
}
type E2eFixtures = {
authedPage: Page
accessToken: string
}
export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
/**
* Override page fixture: BrowserSync keeps a WebSocket open permanently,
* preventing "load" and "networkidle" from resolving. We default all
* navigation methods to "domcontentloaded".
*/
export const test = base.extend({
page: async ({ page }, use) => {
const monitor = attachConsoleMonitor(page)
@@ -47,61 +22,8 @@ export const test = base.extend<E2eFixtures, E2eWorkerFixtures>({
await use(page)
monitor.assertNoErrors()
},
// Worker-scoped: create/reuse test user once per worker
testUser: [
async ({ playwright }, use) => {
const baseURL = process.env.CODING_URL || "https://localhost:3000"
const user = await ensureTestUser(baseURL)
await use(user)
},
{ scope: "worker" },
],
accessToken: async ({ testUser }, use) => {
await use(testUser.accessToken)
},
// Test-scoped: Page with logged-in user via sessionStorage token injection
authedPage: async ({ page, testUser, baseURL }, use) => {
// Navigate to home so domain is set for sessionStorage
await page.goto("/", { waitUntil: "domcontentloaded" })
await page.waitForLoadState("domcontentloaded")
// Inject auth token into sessionStorage (adapt key names to your app)
await page.evaluate(
({ token, user }) => {
sessionStorage.setItem("auth_token", token)
sessionStorage.setItem(
"auth_user",
JSON.stringify({
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
})
)
},
{
token: testUser.accessToken,
user: testUser,
}
)
// Reload so the app reads the token from sessionStorage
await page.reload({ waitUntil: "domcontentloaded" })
await page.waitForLoadState("domcontentloaded")
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
await use(page)
},
})
// ── Helpers ──────────────────────────────────────────────────────────────────
/**
* Wait for the SPA to be ready (appContainer rendered).
* Returns the detected language prefix from the URL.
*/
export async function waitForSpaReady(page: Page): Promise<string> {
await page.waitForLoadState("domcontentloaded")
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
@@ -110,9 +32,6 @@ export async function waitForSpaReady(page: Page): Promise<string> {
return match?.[1] || "de"
}
/**
* Navigate to a route, prepending the current language prefix.
*/
export async function navigateToRoute(page: Page, routePath: string): Promise<void> {
const url = page.url()
const match = url.match(/\/([a-z]{2})(\/|$)/)
@@ -123,9 +42,6 @@ export async function navigateToRoute(page: Page, routePath: string): Promise<vo
await expect(page.locator("#appContainer")).not.toBeEmpty({ timeout: 15000 })
}
/**
* Click an SPA link and verify no full page reload occurred.
*/
export async function clickSpaLink(page: Page, linkSelector: string): Promise<void> {
await page.evaluate(() => {
;(window as any).__spa_navigation_marker = true
+35 -29
View File
@@ -1,44 +1,51 @@
import { test, expect, waitForSpaReady } from "./fixtures"
import { test, expect, clickSpaLink, waitForSpaReady } from "./fixtures"
import { SEEDED_TEST_CONTENT } from "../fixtures/test-constants"
test.describe("Home Page", () => {
test("should load the start page", async ({ page }) => {
await page.goto("/de/")
test.describe("Seeded Home Page", () => {
test("renders the seeded German page with the expected block content", async ({ page }) => {
await page.goto(`/de${SEEDED_TEST_CONTENT.home.path}`)
const lang = await waitForSpaReady(page)
expect(lang).toBe("de")
await expect(page.getByRole("heading", { level: 1, name: "Playwright Seed Startseite" })).toBeVisible()
await expect(page.locator("[data-block='features'] .feature-card")).toHaveCount(3)
await expect(page.locator("[data-block='richtext']")).toContainText(
"Dieser Richtext-Block prueft, dass formatierter HTML-Inhalt im SPA gerendert wird."
)
await expect(page.locator("[data-block='accordion'] button[aria-expanded]").first()).toHaveAttribute(
"aria-expanded",
"true"
)
})
test("should have a visible header with navigation", async ({ page }) => {
await page.goto("/de/")
test("keeps the route stable when switching the language", async ({ page }) => {
await page.goto(`/de${SEEDED_TEST_CONTENT.home.path}`)
await waitForSpaReady(page)
const header = page.locator("header")
await expect(header).toBeVisible()
const nav = header.locator("nav")
await expect(nav).toBeVisible()
})
test("should have language prefix in URL", async ({ page }) => {
await page.goto("/de/")
await waitForSpaReady(page)
expect(page.url()).toContain("/de")
})
test("should switch language to English", async ({ page }) => {
await page.goto("/de/")
await header.getByRole("link", { name: "en", exact: true }).click()
await waitForSpaReady(page)
// Click on English language link
const enLink = page.locator('a[href*="/en"]').first()
if (await enLink.isVisible()) {
await enLink.click()
await page.waitForLoadState("domcontentloaded")
expect(page.url()).toContain("/en")
}
await expect(page).toHaveURL(new RegExp(`/en${SEEDED_TEST_CONTENT.home.path}$`))
await expect(page.getByRole("heading", { level: 1, name: "Playwright Seed Home" })).toBeVisible()
})
test("should allow skipping directly to main content", async ({ page }) => {
await page.goto("/de/")
test("navigates to the seeded contact page via SPA CTA and keeps the form usable", async ({ page }) => {
await page.goto(`/de${SEEDED_TEST_CONTENT.home.path}`)
await waitForSpaReady(page)
await clickSpaLink(page, "[data-block='hero'] a")
await expect(page).toHaveURL(new RegExp(`/de${SEEDED_TEST_CONTENT.contact.path}$`))
await expect(page.locator("[data-block='contact-form']")).toBeVisible()
await expect(page.getByLabel("Name")).toBeVisible()
await expect(page.getByLabel("E-Mail")).toBeVisible()
await expect(page.locator("textarea[name='message']")).toBeVisible()
})
test("moves focus to the main content via the skip link", async ({ page }) => {
await page.goto(`/de${SEEDED_TEST_CONTENT.home.path}`)
await waitForSpaReady(page)
await page.keyboard.press("Tab")
@@ -50,6 +57,5 @@ test.describe("Home Page", () => {
const mainContent = page.locator("main#main-content")
await expect(mainContent).toBeFocused()
await expect(page).toHaveURL(/#main-content$/)
})
})