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:
2026-05-17 00:52:41 +00:00
parent 958b45272d
commit 4020ad62c5
44 changed files with 4276 additions and 867 deletions
+28 -1
View File
@@ -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
View File
@@ -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++
}