✨ feat: enhance medialib image handling and add asset URL resolution
- Implemented `resolveApiAssetUrl` function to normalize asset URLs based on API base. - Updated `MedialibImage` component to utilize new asset URL resolution and added support for alt text and class properties. - Enhanced image loading behavior with improved width measurement and focal point handling. - Added placeholder image handling and improved accessibility with alt text. - Introduced new test script for auditing broken links in skill documentation. - Expanded seeded test content to include medialib entries and updated related tests for pagebuilder previews. - Improved global setup and teardown logging for clarity on seeded content management.
This commit is contained in:
@@ -4,6 +4,12 @@ Playwright tests for E2E, API, mobile, and visual regression. Config in `playwri
|
||||
|
||||
For the full current workflow, prefer the `playwright-testing` skill.
|
||||
|
||||
## Related skills
|
||||
|
||||
- `content-authoring` when test failures may be caused by collection, block-registry, or typing drift.
|
||||
- `media-seo-publishing` when the failing surface includes medialib/image rendering or shared widget behavior.
|
||||
- `tibi-hook-authoring` or `tibi-actions-and-forms` when API behavior under test is defined by hooks or actions instead of plain CRUD.
|
||||
|
||||
## Running tests
|
||||
|
||||
- All tests: `yarn test`
|
||||
@@ -44,6 +50,7 @@ BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `lo
|
||||
- `e2e/fixtures.ts` — Shared desktop helpers (`waitForSpaReady`, `navigateToRoute`, `clickSpaLink`) with BrowserSync-safe navigation defaults.
|
||||
- `e2e-admin/fixtures.ts` — Shared admin helpers (`loginToAdmin`, `openNovaProjectDashboard`) for committed admin smoke coverage.
|
||||
- `e2e-admin/content-config.spec.ts` — Checks that collection config is actually reflected in Nova: sensible list columns/previews, usable widgets, and working pagebuilder preview.
|
||||
- `e2e-admin/pagebuilder.spec.ts` — Checks that the configured pagebuilder block registry renders seeded preview blocks, including an image-backed block via the shared media widget.
|
||||
- `e2e-visual/fixtures.ts` — Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`).
|
||||
- `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
|
||||
- `api/fixtures.ts` — API fixtures (`api`, `adminApi`).
|
||||
@@ -51,6 +58,8 @@ BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `lo
|
||||
- `fixtures/test-constants.ts` — Central constants (`ADMIN_TOKEN`, `API_BASE`, `TEST_BASE_URL`, `SEEDED_TEST_CONTENT`).
|
||||
- Use committed admin smoke tests for stable admin contracts. Use one-shot MCP/browser checks only as exploratory supplements, not as the sole regression guard for important admin paths.
|
||||
- Use admin tests specifically to catch broken collection configuration: empty/bad list previews, missing widgets, broken dependsOn behavior, or non-rendering pagebuilder previews.
|
||||
- Prefer at least one seeded admin preview test that covers a real image-backed pagebuilder block. That catches broken block registries, missing `_lookup` hydration, and admin-incompatible media URL handling early.
|
||||
- Keep test fixtures aligned with the real shared frontend contract; do not add test-only media URL rules or duplicate block-rendering assumptions that diverge from runtime code.
|
||||
- `api/helpers/test-user.ts` is legacy starter scaffolding and should only be reused if the project really needs JWT-user coverage again.
|
||||
|
||||
## Visual regression
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
import { APIRequestContext, request } from "@playwright/test"
|
||||
|
||||
declare const process: {
|
||||
env: Record<string, string | undefined>
|
||||
cwd(): string
|
||||
}
|
||||
|
||||
declare function require(name: string): any
|
||||
|
||||
function detectMaildevUrlFromProject(): string | null {
|
||||
const projectName =
|
||||
process.env.PROJECT_NAME ||
|
||||
(() => {
|
||||
try {
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const envPath = path.resolve(process.cwd(), ".env")
|
||||
if (fs.existsSync(envPath)) {
|
||||
const match = fs.readFileSync(envPath, "utf8").match(/PROJECT_NAME=(.+)/)
|
||||
return match ? match[1].trim() : null
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null
|
||||
})()
|
||||
|
||||
return projectName ? `https://${projectName}-maildev.code.testversion.online` : null
|
||||
}
|
||||
|
||||
/**
|
||||
* MailDev API helper for testing email flows.
|
||||
*
|
||||
@@ -8,7 +35,7 @@ import { APIRequestContext, request } from "@playwright/test"
|
||||
* - MAILDEV_USER: Basic auth username (default: code)
|
||||
* - MAILDEV_PASS: Basic auth password
|
||||
*/
|
||||
const MAILDEV_URL = process.env.MAILDEV_URL || "http://localhost:1080"
|
||||
const MAILDEV_URL = process.env.MAILDEV_URL || detectMaildevUrlFromProject() || "http://localhost:1080"
|
||||
const MAILDEV_USER = process.env.MAILDEV_USER || "code"
|
||||
const MAILDEV_PASS = process.env.MAILDEV_PASS || ""
|
||||
|
||||
|
||||
+349
-247
@@ -7,6 +7,7 @@ type ContentEntry = {
|
||||
_testdata?: boolean
|
||||
translationKey?: string
|
||||
path?: string
|
||||
title?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -19,6 +20,8 @@ function getEntryId(entry: ContentEntry): string | undefined {
|
||||
|
||||
const SEEDED_TRANSLATION_KEYS = new Set<string>(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.translationKey))
|
||||
const SEEDED_PATHS = new Set<string>(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.path))
|
||||
const SEEDED_MEDIA_DATA_URI =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////fwAJ+wP9KobjigAAAABJRU5ErkJggg=="
|
||||
|
||||
function isSeededContentEntry(entry: ContentEntry): boolean {
|
||||
if (entry._testdata === true) {
|
||||
@@ -36,257 +39,353 @@ function isSeededContentEntry(entry: ContentEntry): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const SEEDED_CONTENT_ENTRIES = [
|
||||
function isSeededMedialibEntry(entry: ContentEntry): boolean {
|
||||
return entry._testdata === true
|
||||
}
|
||||
|
||||
const SEEDED_MEDIALIB_ENTRIES = [
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
|
||||
name: "Playwright Startseite",
|
||||
path: SEEDED_TEST_CONTENT.home.path,
|
||||
teaserText: "Deterministisch erzeugte Testseite fuer API- und E2E-Tests.",
|
||||
meta: {
|
||||
title: "Playwright Startseite",
|
||||
description: "Seeded Startseite fuer stabile Playwright-Tests.",
|
||||
keywords: ["playwright", "seed", "e2e"],
|
||||
title: "Playwright Seed Image",
|
||||
alt: {
|
||||
de: "Playwright Seed Bild",
|
||||
en: "Playwright seed image",
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Playwright Seed Startseite",
|
||||
headlineH1: true,
|
||||
tagline: "Deterministische Testdaten",
|
||||
subline: "Diese Seite wird vor dem Testlauf frisch ueber die Admin-API angelegt.",
|
||||
containerWidth: "wide",
|
||||
callToAction: {
|
||||
buttonText: "Zum Kontakt",
|
||||
buttonLink: `/de${SEEDED_TEST_CONTENT.contact.path}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "features",
|
||||
anchorId: "seed-features",
|
||||
headline: "Stabile Grundlage fuer Frontend-Tests",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
featureBoxes: [
|
||||
{
|
||||
icon: "lightning",
|
||||
title: "Frisch angelegt",
|
||||
text: "Die Inhalte werden in globalSetup erstellt statt aus Demo-Daten uebernommen.",
|
||||
},
|
||||
{
|
||||
icon: "database",
|
||||
title: "API-nah",
|
||||
text: "Die Seed-Daten kommen ueber dieselben Collection-Endpunkte wie das CMS.",
|
||||
},
|
||||
{
|
||||
icon: "globe",
|
||||
title: "Mehrsprachig",
|
||||
text: "DE und EN teilen sich denselben translationKey fuer Routing-Checks.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "seed-richtext",
|
||||
headline: "Mehr Kontext",
|
||||
tagline: "API + UI",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>Dieser Richtext-Block prueft, dass formatierter HTML-Inhalt im SPA gerendert wird.</p>",
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
anchorId: "seed-faq",
|
||||
headline: "Hauefige Fragen",
|
||||
tagline: "Verhalten",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
accordionItems: [
|
||||
{
|
||||
question: "Warum Seed-Daten?",
|
||||
answer: "<p>Damit Tests nicht blind auf bestehende Inhalte oder Demo-Routen vertrauen.</p>",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
question: "Was wird geprueft?",
|
||||
answer: "<p>API-Antworten, Routing, Sprachwechsel und Block-Rendering.</p>",
|
||||
open: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "en",
|
||||
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
|
||||
name: "Playwright Home",
|
||||
path: SEEDED_TEST_CONTENT.home.path,
|
||||
teaserText: "Deterministically seeded page for API and E2E coverage.",
|
||||
meta: {
|
||||
title: "Playwright Home",
|
||||
description: "Seeded home page for stable Playwright tests.",
|
||||
keywords: ["playwright", "seed", "home"],
|
||||
description: "Deterministisches Medialib-Bild fuer Admin- und Preview-Tests.",
|
||||
tags: ["playwright", "seed", "preview"],
|
||||
file: {
|
||||
src: SEEDED_MEDIA_DATA_URI,
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Playwright Seed Home",
|
||||
headlineH1: true,
|
||||
tagline: "Deterministic fixtures",
|
||||
subline: "This page is recreated before every test run through the admin API.",
|
||||
containerWidth: "wide",
|
||||
callToAction: {
|
||||
buttonText: "Go to contact",
|
||||
buttonLink: `/en${SEEDED_TEST_CONTENT.contact.path}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "features",
|
||||
anchorId: "seed-features",
|
||||
headline: "Stable frontend coverage",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
featureBoxes: [
|
||||
{
|
||||
icon: "lightning",
|
||||
title: "Freshly created",
|
||||
text: "The content is created during globalSetup instead of relying on demo data.",
|
||||
},
|
||||
{
|
||||
icon: "database",
|
||||
title: "API-backed",
|
||||
text: "The seed uses the same collection endpoints as the CMS itself.",
|
||||
},
|
||||
{
|
||||
icon: "globe",
|
||||
title: "Localized",
|
||||
text: "DE and EN share the same translationKey for route switching checks.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "seed-richtext",
|
||||
headline: "More context",
|
||||
tagline: "API + UI",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>This richtext block proves that formatted HTML content renders in the SPA.</p>",
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
anchorId: "seed-faq",
|
||||
headline: "Common questions",
|
||||
tagline: "Behavior",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
accordionItems: [
|
||||
{
|
||||
question: "Why seeded data?",
|
||||
answer: "<p>So the tests do not depend on existing demo pages or editorial content.</p>",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
question: "What is covered?",
|
||||
answer: "<p>API responses, routing, locale switching and block rendering.</p>",
|
||||
open: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
|
||||
name: "Playwright Kontakt",
|
||||
path: SEEDED_TEST_CONTENT.contact.path,
|
||||
teaserText: "Seeded Kontaktseite fuer Formular- und Routing-Tests.",
|
||||
meta: {
|
||||
title: "Playwright Kontakt",
|
||||
description: "Seeded Kontaktseite fuer Playwright.",
|
||||
keywords: ["playwright", "kontakt"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Kontakt fuer Testlauf",
|
||||
headlineH1: true,
|
||||
tagline: "Seed",
|
||||
subline: "Diese Seite prueft das aktuelle ContactForm-Rendering.",
|
||||
},
|
||||
{
|
||||
type: "contact-form",
|
||||
anchorId: "kontaktformular",
|
||||
headline: "Schreibe uns",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "en",
|
||||
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
|
||||
name: "Playwright Contact",
|
||||
path: SEEDED_TEST_CONTENT.contact.path,
|
||||
teaserText: "Seeded contact page for form and routing tests.",
|
||||
meta: {
|
||||
title: "Playwright Contact",
|
||||
description: "Seeded contact page for Playwright.",
|
||||
keywords: ["playwright", "contact"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Contact for the test run",
|
||||
headlineH1: true,
|
||||
tagline: "Seed",
|
||||
subline: "This page verifies the current contact form rendering.",
|
||||
},
|
||||
{
|
||||
type: "contact-form",
|
||||
anchorId: "contact-form",
|
||||
headline: "Write to us",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: false,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.inactive.translationKey,
|
||||
name: "Playwright Inaktiv",
|
||||
path: SEEDED_TEST_CONTENT.inactive.path,
|
||||
teaserText: "Nicht aktive Seed-Seite fuer 404-Checks.",
|
||||
meta: {
|
||||
title: "Playwright Inaktiv",
|
||||
description: "Nicht aktive Seed-Seite fuer Routing-Tests.",
|
||||
keywords: ["playwright", "inactive"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "inactive",
|
||||
headline: "Sollte nicht sichtbar sein",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>Diese Seite ist absichtlich inaktiv und darf im Frontend nicht erscheinen.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
function getSeededContentEntries(previewImageId: string) {
|
||||
return [
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
|
||||
name: "Playwright Startseite",
|
||||
path: SEEDED_TEST_CONTENT.home.path,
|
||||
teaserText: "Deterministisch erzeugte Testseite fuer API- und E2E-Tests.",
|
||||
meta: {
|
||||
title: "Playwright Startseite",
|
||||
description: "Seeded Startseite fuer stabile Playwright-Tests.",
|
||||
keywords: ["playwright", "seed", "e2e"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Playwright Seed Startseite",
|
||||
headlineH1: true,
|
||||
tagline: "Deterministische Testdaten",
|
||||
subline: "Diese Seite wird vor dem Testlauf frisch ueber die Admin-API angelegt.",
|
||||
containerWidth: "wide",
|
||||
callToAction: {
|
||||
buttonText: "Zum Kontakt",
|
||||
buttonLink: `/de${SEEDED_TEST_CONTENT.contact.path}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "features",
|
||||
anchorId: "seed-features",
|
||||
headline: "Stabile Grundlage fuer Frontend-Tests",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
featureBoxes: [
|
||||
{
|
||||
icon: "lightning",
|
||||
title: "Frisch angelegt",
|
||||
text: "Die Inhalte werden in globalSetup erstellt statt aus Demo-Daten uebernommen.",
|
||||
},
|
||||
{
|
||||
icon: "database",
|
||||
title: "API-nah",
|
||||
text: "Die Seed-Daten kommen ueber dieselben Collection-Endpunkte wie das CMS.",
|
||||
},
|
||||
{
|
||||
icon: "globe",
|
||||
title: "Mehrsprachig",
|
||||
text: "DE und EN teilen sich denselben translationKey fuer Routing-Checks.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "seed-richtext",
|
||||
headline: "Mehr Kontext",
|
||||
tagline: "API + UI",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>Dieser Richtext-Block prueft, dass formatierter HTML-Inhalt im SPA gerendert wird.</p>",
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
anchorId: "seed-faq",
|
||||
headline: "Hauefige Fragen",
|
||||
tagline: "Verhalten",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
accordionItems: [
|
||||
{
|
||||
question: "Warum Seed-Daten?",
|
||||
answer: "<p>Damit Tests nicht blind auf bestehende Inhalte oder Demo-Routen vertrauen.</p>",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
question: "Was wird geprueft?",
|
||||
answer: "<p>API-Antworten, Routing, Sprachwechsel und Block-Rendering.</p>",
|
||||
open: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "en",
|
||||
translationKey: SEEDED_TEST_CONTENT.home.translationKey,
|
||||
name: "Playwright Home",
|
||||
path: SEEDED_TEST_CONTENT.home.path,
|
||||
teaserText: "Deterministically seeded page for API and E2E coverage.",
|
||||
meta: {
|
||||
title: "Playwright Home",
|
||||
description: "Seeded home page for stable Playwright tests.",
|
||||
keywords: ["playwright", "seed", "home"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Playwright Seed Home",
|
||||
headlineH1: true,
|
||||
tagline: "Deterministic fixtures",
|
||||
subline: "This page is recreated before every test run through the admin API.",
|
||||
containerWidth: "wide",
|
||||
callToAction: {
|
||||
buttonText: "Go to contact",
|
||||
buttonLink: `/en${SEEDED_TEST_CONTENT.contact.path}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "features",
|
||||
anchorId: "seed-features",
|
||||
headline: "Stable frontend coverage",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
featureBoxes: [
|
||||
{
|
||||
icon: "lightning",
|
||||
title: "Freshly created",
|
||||
text: "The content is created during globalSetup instead of relying on demo data.",
|
||||
},
|
||||
{
|
||||
icon: "database",
|
||||
title: "API-backed",
|
||||
text: "The seed uses the same collection endpoints as the CMS itself.",
|
||||
},
|
||||
{
|
||||
icon: "globe",
|
||||
title: "Localized",
|
||||
text: "DE and EN share the same translationKey for route switching checks.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "seed-richtext",
|
||||
headline: "More context",
|
||||
tagline: "API + UI",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>This richtext block proves that formatted HTML content renders in the SPA.</p>",
|
||||
},
|
||||
{
|
||||
type: "accordion",
|
||||
anchorId: "seed-faq",
|
||||
headline: "Common questions",
|
||||
tagline: "Behavior",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
accordionItems: [
|
||||
{
|
||||
question: "Why seeded data?",
|
||||
answer: "<p>So the tests do not depend on existing demo pages or editorial content.</p>",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
question: "What is covered?",
|
||||
answer: "<p>API responses, routing, locale switching and block rendering.</p>",
|
||||
open: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.pagebuilderPreview.translationKey,
|
||||
name: "Playwright Pagebuilder Preview",
|
||||
path: SEEDED_TEST_CONTENT.pagebuilderPreview.path,
|
||||
teaserText: "Seeded Admin-Vorschauseite fuer Pagebuilder-Registry und Bildrendering.",
|
||||
meta: {
|
||||
title: "Playwright Pagebuilder Preview",
|
||||
description: "Seeded Seite fuer Pagebuilder- und Medialib-Vorschau-Tests.",
|
||||
keywords: ["playwright", "pagebuilder", "preview"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Playwright Registry Hero",
|
||||
headlineH1: true,
|
||||
tagline: "Admin Preview",
|
||||
subline: "Dieses Hero-Preview prueft den Block-Registry-Pfad inklusive Bild.",
|
||||
containerWidth: "wide",
|
||||
heroImage: {
|
||||
image: previewImageId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "pagebuilder-preview-richtext",
|
||||
headline: "Richtext mit Bild",
|
||||
tagline: "Shared Media Widget",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "right",
|
||||
image: previewImageId,
|
||||
text: "<p>Dieser Block prueft, dass ein image-gestuetzter Preview-Block im Admin ueber dieselbe Registry und dasselbe Bild-Widget rendert.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
|
||||
name: "Playwright Kontakt",
|
||||
path: SEEDED_TEST_CONTENT.contact.path,
|
||||
teaserText: "Seeded Kontaktseite fuer Formular- und Routing-Tests.",
|
||||
meta: {
|
||||
title: "Playwright Kontakt",
|
||||
description: "Seeded Kontaktseite fuer Playwright.",
|
||||
keywords: ["playwright", "kontakt"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Kontakt fuer Testlauf",
|
||||
headlineH1: true,
|
||||
tagline: "Seed",
|
||||
subline: "Diese Seite prueft das aktuelle ContactForm-Rendering.",
|
||||
},
|
||||
{
|
||||
type: "contact-form",
|
||||
anchorId: "kontaktformular",
|
||||
headline: "Schreibe uns",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: true,
|
||||
type: "page",
|
||||
lang: "en",
|
||||
translationKey: SEEDED_TEST_CONTENT.contact.translationKey,
|
||||
name: "Playwright Contact",
|
||||
path: SEEDED_TEST_CONTENT.contact.path,
|
||||
teaserText: "Seeded contact page for form and routing tests.",
|
||||
meta: {
|
||||
title: "Playwright Contact",
|
||||
description: "Seeded contact page for Playwright.",
|
||||
keywords: ["playwright", "contact"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "hero",
|
||||
headline: "Contact for the test run",
|
||||
headlineH1: true,
|
||||
tagline: "Seed",
|
||||
subline: "This page verifies the current contact form rendering.",
|
||||
},
|
||||
{
|
||||
type: "contact-form",
|
||||
anchorId: "contact-form",
|
||||
headline: "Write to us",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_testdata: true,
|
||||
active: false,
|
||||
type: "page",
|
||||
lang: "de",
|
||||
translationKey: SEEDED_TEST_CONTENT.inactive.translationKey,
|
||||
name: "Playwright Inaktiv",
|
||||
path: SEEDED_TEST_CONTENT.inactive.path,
|
||||
teaserText: "Nicht aktive Seed-Seite fuer 404-Checks.",
|
||||
meta: {
|
||||
title: "Playwright Inaktiv",
|
||||
description: "Nicht aktive Seed-Seite fuer Routing-Tests.",
|
||||
keywords: ["playwright", "inactive"],
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
type: "richtext",
|
||||
anchorId: "inactive",
|
||||
headline: "Sollte nicht sichtbar sein",
|
||||
tagline: "Seed",
|
||||
padding: { top: "md", bottom: "md" },
|
||||
imagePosition: "none",
|
||||
text: "<p>Diese Seite ist absichtlich inaktiv und darf im Frontend nicht erscheinen.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const
|
||||
}
|
||||
|
||||
async function cleanupSeededMedialibEntries(baseURL: string): Promise<number> {
|
||||
const medialibEntries = await listCollectionEntries<ContentEntry>(baseURL, "medialib")
|
||||
const seededEntries = medialibEntries.filter((entry) => isSeededMedialibEntry(entry))
|
||||
|
||||
let deleted = 0
|
||||
for (const entry of seededEntries) {
|
||||
const entryId = getEntryId(entry)
|
||||
if (!entryId) continue
|
||||
|
||||
const ok = await deleteCollectionEntry(baseURL, "medialib", entryId)
|
||||
if (ok) deleted++
|
||||
}
|
||||
|
||||
return deleted
|
||||
}
|
||||
|
||||
async function seedMedialibEntries(baseURL: string): Promise<{ created: number; previewImageId: string }> {
|
||||
let previewImageId = ""
|
||||
let created = 0
|
||||
|
||||
for (const entry of SEEDED_MEDIALIB_ENTRIES) {
|
||||
const createdEntry = await createCollectionEntry<ContentEntry>(baseURL, "medialib", {
|
||||
...(entry as Record<string, unknown>),
|
||||
})
|
||||
const entryId = getEntryId(createdEntry)
|
||||
if (!entryId) {
|
||||
throw new Error("Seeded medialib entry was created without an id")
|
||||
}
|
||||
|
||||
previewImageId = entryId
|
||||
created++
|
||||
}
|
||||
|
||||
return { created, previewImageId }
|
||||
}
|
||||
|
||||
export async function cleanupSeededTestContent(baseURL: string): Promise<number> {
|
||||
const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content")
|
||||
// Cleanup runs before every seed pass so leftovers from aborted test runs
|
||||
@@ -302,12 +401,15 @@ export async function cleanupSeededTestContent(baseURL: string): Promise<number>
|
||||
if (ok) deleted++
|
||||
}
|
||||
|
||||
return deleted
|
||||
const deletedMediaEntries = await cleanupSeededMedialibEntries(baseURL)
|
||||
return deleted + deletedMediaEntries
|
||||
}
|
||||
|
||||
export async function seedTestContent(baseURL: string): Promise<number> {
|
||||
let created = 0
|
||||
for (const entry of SEEDED_CONTENT_ENTRIES) {
|
||||
const mediaSeed = await seedMedialibEntries(baseURL)
|
||||
let created = mediaSeed.created
|
||||
|
||||
for (const entry of getSeededContentEntries(mediaSeed.previewImageId)) {
|
||||
await createCollectionEntry(baseURL, "content", entry as unknown as Record<string, unknown>)
|
||||
created++
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ export const test = base.extend({
|
||||
export async function loginToAdmin(page: Page): Promise<void> {
|
||||
await page.goto(`${TEST_ADMIN_BASE_URL}/login`)
|
||||
|
||||
// Admin bootet als SPA; das Formular ist oft erst nach Chunk-Ladevorgang interaktiv.
|
||||
await expect(page.getByLabel(/Benutzername|Username/i)).toBeVisible({ timeout: 20000 })
|
||||
|
||||
await page.getByLabel(/Benutzername|Username/i).fill(ADMIN_UI_CREDENTIALS.username)
|
||||
await page.getByLabel(/Passwort|Password/i).fill(ADMIN_UI_CREDENTIALS.password)
|
||||
await page.getByRole("button", { name: /Anmelden|Sign in|Login/i }).click()
|
||||
@@ -55,6 +58,17 @@ export async function openNewContentEntry(page: Page): Promise<void> {
|
||||
await openNewCollectionEntry(page, /Inhalte/, "content", "Inhalte")
|
||||
}
|
||||
|
||||
export async function openContentEntry(page: Page, rowText: string | RegExp): Promise<void> {
|
||||
await openContentCollection(page)
|
||||
|
||||
const entryRow = page.getByRole("row").filter({ hasText: rowText }).first()
|
||||
await expect(entryRow).toBeVisible()
|
||||
await entryRow.click()
|
||||
|
||||
await expect(page).toHaveURL(/\/collections\/content\/entries\/[^/?]+/)
|
||||
await expect(page.locator("main")).toBeVisible()
|
||||
}
|
||||
|
||||
export async function openNavigationCollection(page: Page): Promise<void> {
|
||||
await openCollection(page, /Navigation/, "navigation", "Navigation")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { test, expect, openContentEntry } from "./fixtures"
|
||||
|
||||
test.describe("Admin pagebuilder registry and preview rendering", () => {
|
||||
test("renders seeded image-backed blocks through the configured pagebuilder registry", async ({ page }) => {
|
||||
await openContentEntry(page, /Playwright Pagebuilder Preview/)
|
||||
|
||||
const previewRoot = page.locator("[data-admin-preview]")
|
||||
const heroPreview = previewRoot.locator("[data-block='hero']").first()
|
||||
const richtextPreview = previewRoot.locator("[data-block='richtext']").first()
|
||||
|
||||
await expect(previewRoot.first()).toBeVisible()
|
||||
await expect(heroPreview.getByRole("heading", { name: "Playwright Registry Hero" })).toBeVisible()
|
||||
await expect(richtextPreview).toContainText("Richtext mit Bild")
|
||||
await expect(richtextPreview).toContainText("dass ein image-gestuetzter Preview-Block")
|
||||
|
||||
const previewImages = previewRoot.locator("img[data-entry-id]")
|
||||
await expect(previewImages.first()).toBeVisible()
|
||||
await expect(previewImages.first()).toHaveAttribute("src", /\/medialib\/[^/]+\/file\/[^?]+(?:\?filter=[^"']+)?/)
|
||||
await expect(previewImages).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
Vendored
+4
@@ -57,6 +57,10 @@ export const SEEDED_TEST_CONTENT = {
|
||||
translationKey: "pw-e2e-home",
|
||||
path: "/playwright-e2e-home",
|
||||
},
|
||||
pagebuilderPreview: {
|
||||
translationKey: "pw-e2e-pagebuilder-preview",
|
||||
path: "/playwright-e2e-pagebuilder-preview",
|
||||
},
|
||||
contact: {
|
||||
translationKey: "pw-e2e-contact",
|
||||
path: "/playwright-e2e-contact",
|
||||
|
||||
@@ -34,7 +34,7 @@ async function globalSetup() {
|
||||
|
||||
const result = await ensureSeededTestContent(baseURL)
|
||||
console.log(
|
||||
`Playwright setup: seeded deterministic content (deleted ${result.deleted}, created ${result.created})`
|
||||
`Playwright setup: seeded deterministic test data (deleted ${result.deleted}, created ${result.created})`
|
||||
)
|
||||
} finally {
|
||||
await ctx.dispose()
|
||||
|
||||
@@ -28,7 +28,7 @@ async function globalTeardown() {
|
||||
const result = await cleanupAllTestData(baseURL)
|
||||
if (deletedSeedEntries > 0 || result.users > 0) {
|
||||
console.log(
|
||||
`Playwright teardown: deleted ${deletedSeedEntries} seeded content entries and ${result.users} test users`
|
||||
`Playwright teardown: deleted ${deletedSeedEntries} seeded content/media entries and ${result.users} test users`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user