✨ 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:
+35
-170
@@ -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
@@ -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
@@ -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$/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user