feat: add admin smoke tests and enhance testing documentation with new strategies and configurations

This commit is contained in:
2026-05-12 21:01:25 +00:00
parent c058ec760f
commit 53ad012657
14 changed files with 388 additions and 51 deletions
+101 -1
View File
@@ -23,6 +23,7 @@ This starter uses Playwright across four slices:
- `tests/api/` for API-level checks - `tests/api/` for API-level checks
- `tests/e2e/` for desktop browser behavior - `tests/e2e/` for desktop browser behavior
- `tests/e2e-admin/` for committed admin smoke coverage
- `tests/e2e-mobile/` for mobile behavior - `tests/e2e-mobile/` for mobile behavior
- `tests/e2e-visual/` for screenshot-based regression tests - `tests/e2e-visual/` for screenshot-based regression tests
@@ -40,6 +41,7 @@ The current baseline is deterministic and seed-driven, not demo-content-driven.
| `tests/api/helpers/seed-data.ts` | Seed definitions and seed cleanup for deterministic content pages | | `tests/api/helpers/seed-data.ts` | Seed definitions and seed cleanup for deterministic content pages |
| `tests/fixtures/console-monitor.ts` | Fails browser-based tests on unexpected page, console, or request errors | | `tests/fixtures/console-monitor.ts` | Fails browser-based tests on unexpected page, console, or request errors |
| `tests/e2e/fixtures.ts` | Desktop browser fixtures and SPA helpers | | `tests/e2e/fixtures.ts` | Desktop browser fixtures and SPA helpers |
| `tests/e2e-admin/fixtures.ts` | Admin login helpers and admin smoke fixture setup |
| `tests/e2e-mobile/fixtures.ts` | Mobile browser fixtures and hamburger-menu helpers | | `tests/e2e-mobile/fixtures.ts` | Mobile browser fixtures and hamburger-menu helpers |
--- ---
@@ -63,6 +65,23 @@ For this project, prefer the reverse-proxied `CODING_URL` from `.env` whenever i
If `/api/...` returns HTML instead of JSON, the seeded setup is not usable and `globalSetup` should fail fast. If `/api/...` returns HTML instead of JSON, the seeded setup is not usable and `globalSetup` should fail fast.
### Admin host and default credentials
Admin browser tests use `TEST_ADMIN_BASE_URL` from `tests/fixtures/test-constants.ts`.
Resolution order:
1. `process.env.CODING_TIBIADMIN_URL`
2. `CODING_TIBIADMIN_URL` from `.env`
3. fallback `http://localhost:3000`
The current smoke setup assumes the default dev login unless overridden via env vars:
- `ADMIN_UI_USERNAME` default: `admin`
- `ADMIN_UI_PASSWORD` default: `admin`
Keep this only for local/dev smoke coverage. Do not turn production credentials into committed test defaults.
### Static project token vs JWT user auth ### Static project token vs JWT user auth
This distinction matters for tests: This distinction matters for tests:
@@ -117,15 +136,46 @@ Do not silence app bugs by broadening ignored patterns unless the noise is clear
The current setup seeds content through the public collection API plus the static `Token:` header. The current setup seeds content through the public collection API plus the static `Token:` header.
Use a hidden per-collection marker field as the default seed identity strategy.
In this project the convention is `_testdata: true`.
### Seed lifecycle ### Seed lifecycle
1. `globalSetup` probes the configured base URL. 1. `globalSetup` probes the configured base URL.
2. `globalSetup` verifies `/api/content` returns JSON. 2. `globalSetup` verifies `/api/content` returns JSON.
3. `globalSetup` removes old seeded entries by `translationKey`. 3. `globalSetup` removes old seeded entries by their hidden test marker before recreating them.
4. `globalSetup` creates deterministic seed entries. 4. `globalSetup` creates deterministic seed entries.
5. Tests run against those seeded routes. 5. Tests run against those seeded routes.
6. `globalTeardown` removes seeded entries again. 6. `globalTeardown` removes seeded entries again.
Setup cleanup and teardown cleanup are both required.
The setup cleanup handles leftovers from aborted or previously failed runs.
The teardown cleanup keeps the environment clean after successful runs.
### Hidden seed marker pattern
Prefer this pattern for every collection that may receive test-created data:
1. Add a hidden boolean field named `_testdata` as the last field in the collection schema.
2. Set `_testdata: true` on every seeded entry.
3. Let cleanup match `_testdata === true` first.
4. Keep older identifiers such as fixed paths or translation keys only as migration fallbacks when existing seed data already used them.
This is more robust than relying on translation keys because not every collection has a natural grouping field.
It also makes leftovers from aborted runs discoverable across heterogeneous collection shapes.
### Parallel worker rule
Seed creation and seed cleanup must remain run-scoped, not worker-scoped.
- perform seed cleanup and creation in `globalSetup`
- perform final seed cleanup in `globalTeardown`
- do not create or delete shared seeded data in per-test hooks or worker fixtures
- keep seeded identifiers deterministic so many workers can read the same seeded dataset safely
This project runs with many workers.
Parallel safety depends on one shared deterministic seed pass before the suite and one shared cleanup pass after the suite, not on each worker mutating shared fixtures independently.
### Current seeded routes ### Current seeded routes
Defined in `tests/fixtures/test-constants.ts`: Defined in `tests/fixtures/test-constants.ts`:
@@ -173,6 +223,20 @@ Use `tests/e2e/` when validating:
- block rendering in the real UI - block rendering in the real UI
- keyboard/a11y interactions such as skip links - keyboard/a11y interactions such as skip links
### Admin smoke tests
Use `tests/e2e-admin/` when validating stable admin contracts such as:
- admin login still works in dev
- the project dashboard opens correctly
- core collections are still reachable
- critical collection views still render their configured labels/columns/actions
- collection lists render meaningful previews instead of broken placeholders
- important field widgets are configured and usable in entry forms
- pagebuilder block choosers, block forms, and live previews load correctly
These tests should stay intentionally narrow. They are regression guards for admin configuration, not full editor journey automation.
### Mobile E2E tests ### Mobile E2E tests
Use `tests/e2e-mobile/` when validating: Use `tests/e2e-mobile/` when validating:
@@ -185,6 +249,31 @@ Use `tests/e2e-mobile/` when validating:
Use `tests/e2e-visual/` only when layout/styling stability matters and a semantic DOM assertion is not enough. Use `tests/e2e-visual/` only when layout/styling stability matters and a semantic DOM assertion is not enough.
## Admin config coverage strategy
Use a hybrid approach:
- committed Playwright smoke tests for stable, repeatable admin contracts
- one-shot MCP Playwright or VS Code browser checks for exploratory spot checks and ad-hoc audits
Committed tests should cover the admin paths that are expected to stay valid across everyday work, for example:
- login
- opening the Nova project dashboard
- visibility of the core collections
- opening important collection views like `content`
- checking that collection tables expose the intended columns, summaries, and preview thumbnails
- checking that key widgets like selects, foreign/media pickers, sidebars, and pagebuilder controls actually render
- checking that pagebuilder preview updates when block content changes
One-shot live browser checks are useful when:
- reviewing a newly added admin configuration once
- probing a flaky or hard-to-stabilize UI area before deciding what deserves a real test
- checking something highly visual or temporarily environment-specific
Do not rely on one-shot browser checks as the only safeguard for important admin paths. If a check matters repeatedly, promote it into `tests/e2e-admin/`.
--- ---
## Current fixture conventions ## Current fixture conventions
@@ -211,6 +300,16 @@ Helpers include:
- `clickSpaLink(page, selector)` - `clickSpaLink(page, selector)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)` - automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
### Admin E2E
Use `tests/e2e-admin/fixtures.ts`.
Helpers include:
- `loginToAdmin(page)`
- `openNovaProjectDashboard(page)`
- automatic console/page/request error monitoring via `attachConsoleMonitor(page)`
### Mobile E2E ### Mobile E2E
Use `tests/e2e-mobile/fixtures.ts`. Use `tests/e2e-mobile/fixtures.ts`.
@@ -265,6 +364,7 @@ Run only the slice you changed.
```bash ```bash
/usr/bin/node ./node_modules/playwright/cli.js test tests/api/health.spec.ts --project=api /usr/bin/node ./node_modules/playwright/cli.js test tests/api/health.spec.ts --project=api
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e/home.spec.ts tests/e2e/demo.spec.ts --project=chromium /usr/bin/node ./node_modules/playwright/cli.js test tests/e2e/home.spec.ts tests/e2e/demo.spec.ts --project=chromium
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-admin/smoke.spec.ts --project=admin
/usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-mobile/home.mobile.spec.ts --project=mobile-iphonese /usr/bin/node ./node_modules/playwright/cli.js test tests/e2e-mobile/home.mobile.spec.ts --project=mobile-iphonese
``` ```
+4
View File
@@ -459,3 +459,7 @@ fields:
type: string[] type: string[]
meta: meta:
label: { de: "Meta-Schlüsselwörter", en: "Meta Keywords" } label: { de: "Meta-Schlüsselwörter", en: "Meta Keywords" }
- name: _testdata
type: boolean
meta:
hide: true
+4
View File
@@ -118,3 +118,7 @@ fields:
meta: meta:
label: { de: "Tags", en: "Tags" } label: { de: "Tags", en: "Tags" }
widget: chipArray widget: chipArray
- name: _testdata
type: boolean
meta:
hide: true
+4
View File
@@ -115,3 +115,7 @@ fields:
type: object[] type: object[]
meta: meta:
label: { de: "Unterpunkte", en: "Child Items" } label: { de: "Unterpunkte", en: "Child Items" }
- name: _testdata
type: boolean
meta:
hide: true
+5
View File
@@ -59,3 +59,8 @@ fields:
label: label:
de: Abhängigkeiten de: Abhängigkeiten
en: Dependencies en: Dependencies
- name: _testdata
type: boolean
meta:
hide: true
+11 -1
View File
@@ -1,5 +1,5 @@
import { defineConfig, devices } from "@playwright/test" import { defineConfig, devices } from "@playwright/test"
import { TEST_BASE_URL } from "./tests/fixtures/test-constants" import { TEST_ADMIN_BASE_URL, TEST_BASE_URL } from "./tests/fixtures/test-constants"
/** /**
* Playwright configuration for tibi-svelte projects. * Playwright configuration for tibi-svelte projects.
@@ -70,6 +70,16 @@ export default defineConfig({
}, },
}, },
/* ── Admin Smoke Tests ────────────────────────────────────────── */
{
name: "admin",
testDir: "./tests/e2e-admin",
use: {
...withPlaywrightUA(devices["Desktop Chrome"]),
baseURL: TEST_ADMIN_BASE_URL,
},
},
/* ── Mobile E2E ────────────────────────────────────────────────── */ /* ── Mobile E2E ────────────────────────────────────────────────── */
{ {
name: "mobile-iphonese", name: "mobile-iphonese",
+9
View File
@@ -9,11 +9,13 @@ For the full current workflow, prefer the `playwright-testing` skill.
- All tests: `yarn test` - All tests: `yarn test`
- E2E: `yarn test:e2e` - E2E: `yarn test:e2e`
- API: `yarn test:api` - API: `yarn test:api`
- Admin smoke: `npx playwright test tests/e2e-admin/smoke.spec.ts --project=admin`
- Visual regression: `yarn test:visual` - Visual regression: `yarn test:visual`
- Single file: `npx playwright test tests/e2e/filename.spec.ts` - Single file: `npx playwright test tests/e2e/filename.spec.ts`
- Single test: `npx playwright test -g "test name"` - Single test: `npx playwright test -g "test name"`
- After code changes, run only affected spec files — not the full suite. - After code changes, run only affected spec files — not the full suite.
- Prefer the configured `CODING_URL` from `.env` whenever it serves both `/` and `/api/...`. - Prefer the configured `CODING_URL` from `.env` whenever it serves both `/` and `/api/...`.
- Admin browser tests use `CODING_TIBIADMIN_URL` from `.env` and default to `admin/admin` unless overridden via env vars.
- The current test baseline is deterministic and seed-driven, not demo-content-driven. - The current test baseline is deterministic and seed-driven, not demo-content-driven.
## BrowserSync workaround ## BrowserSync workaround
@@ -32,16 +34,23 @@ BrowserSync keeps a WebSocket open permanently, preventing `networkidle` and `lo
- `tests/global-teardown.ts` removes seeded content again and disposes shared API contexts. - `tests/global-teardown.ts` removes seeded content again and disposes shared API contexts.
- Seed data lives in `tests/api/helpers/seed-data.ts`. - Seed data lives in `tests/api/helpers/seed-data.ts`.
- Seeded route constants live in `tests/fixtures/test-constants.ts`. - Seeded route constants live in `tests/fixtures/test-constants.ts`.
- Prefer a hidden `_testdata` boolean field as the last field per collection as the primary marker for seeded test data.
- Cleanup belongs both in `globalSetup` and `globalTeardown`.
- Seed creation/cleanup must stay run-scoped so the suite works with many workers; do not create or delete shared seeded data in per-test hooks.
- If tests write to a collection through `ADMIN_TOKEN`, that collection must define explicit permissions like `"token:${ADMIN_TOKEN}":`. - If tests write to a collection through `ADMIN_TOKEN`, that collection must define explicit permissions like `"token:${ADMIN_TOKEN}":`.
## Fixtures & helpers ## Fixtures & helpers
- `e2e/fixtures.ts` — Shared desktop helpers (`waitForSpaReady`, `navigateToRoute`, `clickSpaLink`) with BrowserSync-safe navigation defaults. - `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-visual/fixtures.ts` — Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`). - `e2e-visual/fixtures.ts` — Visual test helpers (`waitForVisualReady`, `hideDynamicContent`, `prepareForScreenshot`, `expectScreenshot`, `getDynamicMasks`).
- `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`). - `e2e-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`).
- `api/fixtures.ts` — API fixtures (`api`, `adminApi`). - `api/fixtures.ts` — API fixtures (`api`, `adminApi`).
- `api/helpers/` — API test utilities (`admin-api.ts`, `seed-data.ts`, `maildev.ts`). - `api/helpers/` — API test utilities (`admin-api.ts`, `seed-data.ts`, `maildev.ts`).
- `fixtures/test-constants.ts` — Central constants (`ADMIN_TOKEN`, `API_BASE`, `TEST_BASE_URL`, `SEEDED_TEST_CONTENT`). - `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.
- `api/helpers/test-user.ts` is legacy starter scaffolding and should only be reused if the project really needs JWT-user coverage again. - `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 ## Visual regression
+27 -3
View File
@@ -4,7 +4,9 @@ import { createCollectionEntry, deleteCollectionEntry, listCollectionEntries } f
type ContentEntry = { type ContentEntry = {
id?: string id?: string
_id?: string | { $oid?: string } _id?: string | { $oid?: string }
_testdata?: boolean
translationKey?: string translationKey?: string
path?: string
[key: string]: unknown [key: string]: unknown
} }
@@ -16,9 +18,27 @@ function getEntryId(entry: ContentEntry): string | undefined {
} }
const SEEDED_TRANSLATION_KEYS = new Set<string>(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.translationKey)) 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))
function isSeededContentEntry(entry: ContentEntry): boolean {
if (entry._testdata === true) {
return true
}
if (typeof entry.translationKey === "string" && SEEDED_TRANSLATION_KEYS.has(entry.translationKey)) {
return true
}
if (typeof entry.path === "string" && SEEDED_PATHS.has(entry.path)) {
return true
}
return false
}
const SEEDED_CONTENT_ENTRIES = [ const SEEDED_CONTENT_ENTRIES = [
{ {
_testdata: true,
active: true, active: true,
type: "page", type: "page",
lang: "de", lang: "de",
@@ -99,6 +119,7 @@ const SEEDED_CONTENT_ENTRIES = [
], ],
}, },
{ {
_testdata: true,
active: true, active: true,
type: "page", type: "page",
lang: "en", lang: "en",
@@ -179,6 +200,7 @@ const SEEDED_CONTENT_ENTRIES = [
], ],
}, },
{ {
_testdata: true,
active: true, active: true,
type: "page", type: "page",
lang: "de", lang: "de",
@@ -208,6 +230,7 @@ const SEEDED_CONTENT_ENTRIES = [
], ],
}, },
{ {
_testdata: true,
active: true, active: true,
type: "page", type: "page",
lang: "en", lang: "en",
@@ -237,6 +260,7 @@ const SEEDED_CONTENT_ENTRIES = [
], ],
}, },
{ {
_testdata: true,
active: false, active: false,
type: "page", type: "page",
lang: "de", lang: "de",
@@ -265,9 +289,9 @@ const SEEDED_CONTENT_ENTRIES = [
export async function cleanupSeededTestContent(baseURL: string): Promise<number> { export async function cleanupSeededTestContent(baseURL: string): Promise<number> {
const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content") const contentEntries = await listCollectionEntries<ContentEntry>(baseURL, "content")
const seededEntries = contentEntries.filter( // Cleanup runs before every seed pass so leftovers from aborted test runs
(entry) => typeof entry.translationKey === "string" && SEEDED_TRANSLATION_KEYS.has(entry.translationKey) // are removed on the next successful global setup.
) const seededEntries = contentEntries.filter((entry) => isSeededContentEntry(entry))
let deleted = 0 let deleted = 0
for (const entry of seededEntries) { for (const entry of seededEntries) {
+65
View File
@@ -0,0 +1,65 @@
import { test, expect, openContentCollection, openNewContentEntry } from "./fixtures"
test.describe("Admin content collection config", () => {
test("renders a meaningful content list with configured columns and pagebuilder summaries", async ({ page }) => {
await openContentCollection(page)
const main = page.locator("main")
const table = page.getByRole("table")
const homeRow = page.getByRole("row", { name: /Startseite\s+\// }).first()
await expect(table).toBeVisible()
await expect(main).toContainText("Sprache")
await expect(main).toContainText("Pfad")
await expect(main).toContainText("Inhaltsblöcke")
await expect(homeRow).toContainText("Startseite")
await expect(homeRow).toContainText("Hero")
await expect(homeRow).toContainText("Features")
await expect(table.locator("tbody img").first()).toBeVisible()
})
test("shows the configured content widgets in the new entry form", async ({ page }) => {
await openNewContentEntry(page)
await expect(page.getByLabel("Name")).toBeVisible()
await expect(page.getByLabel("Pfad")).toBeVisible()
await expect(page.getByLabel("Teasertext")).toBeVisible()
await expect(page.getByText("Alternative Pfade")).toBeVisible()
await expect(page.getByText("Inhaltsblöcke").first()).toBeVisible()
await expect(page.getByRole("button", { name: /Desktop \(1280px\)/ })).toBeVisible()
await expect(page.getByRole("button", { name: /Tablet \(768px\)/ })).toBeVisible()
await expect(page.getByRole("button", { name: /Mobil \(375px\)/ })).toBeVisible()
await expect(page.getByRole("button", { name: /Block hinzufügen/ }).first()).toBeVisible()
})
test("loads the pagebuilder block chooser and renders the hero preview live", async ({ page }) => {
await openNewContentEntry(page)
await page
.getByRole("button", { name: /Block hinzufügen/ })
.first()
.click()
await expect(page.getByRole("button", { name: /Hero .*Call-to-Action\./ })).toBeVisible()
await expect(page.getByRole("button", { name: /Features .*strukturierten Boxen\./ })).toBeVisible()
await expect(page.getByRole("button", { name: /Richtext .*Bild\./ })).toBeVisible()
await expect(page.getByRole("button", { name: /Akkordeon .*Fragen und Antworten\./ })).toBeVisible()
await expect(page.getByRole("button", { name: /Kontaktformular .*Intro-Text\./ })).toBeVisible()
await page.getByRole("button", { name: /Hero .*Call-to-Action\./ }).click()
const dialog = page.getByRole("dialog", { name: /Hero #1 bearbeiten/ })
await expect(dialog).toBeVisible()
await expect(dialog.getByLabel("Blocktyp")).toBeVisible()
await expect(dialog.getByLabel("Unterzeile")).toBeVisible()
await expect(dialog.getByLabel("Containerbreite")).toBeVisible()
await expect(dialog.getByRole("button", { name: /Vorhandene durchsuchen/ })).toBeVisible()
await dialog.getByLabel("Überschrift").fill("Admin Preview Test")
await dialog.getByLabel("Unterzeile").fill("Pagebuilder Vorschau aktualisiert")
await expect(dialog.getByRole("heading", { name: "Admin Preview Test" })).toBeVisible()
await expect(dialog).toContainText("Pagebuilder Vorschau aktualisiert")
await expect(page.getByRole("button", { name: /Admin Preview Test/ })).toBeVisible()
})
})
+69
View File
@@ -0,0 +1,69 @@
import { test as base, expect, type Page } from "@playwright/test"
import { attachConsoleMonitor } from "../fixtures/console-monitor"
import { ADMIN_UI_CREDENTIALS, TEST_ADMIN_BASE_URL } from "../fixtures/test-constants"
export const test = base.extend({
page: async ({ page }, use) => {
const monitor = attachConsoleMonitor(page)
const origGoto = page.goto.bind(page)
const origReload = page.reload.bind(page)
page.goto = ((url: string, opts?: any) =>
origGoto(url, { waitUntil: "domcontentloaded", ...opts })) as typeof page.goto
page.reload = ((opts?: any) => origReload({ waitUntil: "domcontentloaded", ...opts })) as typeof page.reload
await use(page)
monitor.assertNoErrors()
},
})
export async function loginToAdmin(page: Page): Promise<void> {
await page.goto(`${TEST_ADMIN_BASE_URL}/login`)
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()
await expect(page).toHaveURL(/\/projects\//, { timeout: 15000 })
await expect(page.locator("main")).toBeVisible()
}
export async function openNovaProjectDashboard(page: Page): Promise<void> {
await loginToAdmin(page)
const germanLocaleButton = page.getByRole("button", { name: /^Deutsch$/ })
if ((await germanLocaleButton.count()) > 0) {
await germanLocaleButton.first().click()
}
const openNovaButton = page.getByRole("button", { name: /Nova öffnen|Open Nova/i })
if ((await openNovaButton.count()) > 0 && (await openNovaButton.first().isVisible())) {
await openNovaButton.first().click()
}
await expect(page.getByRole("textbox", { name: /Kollektionen durchsuchen|Search collections/i })).toBeVisible({
timeout: 15000,
})
}
export async function openContentCollection(page: Page): Promise<void> {
await openNovaProjectDashboard(page)
await page
.getByRole("link", { name: /Inhalte/ })
.first()
.click()
await expect(page).toHaveURL(/\/collections\/content$/)
await expect(page.locator("main h1")).toHaveText("Inhalte")
}
export async function openNewContentEntry(page: Page): Promise<void> {
await openContentCollection(page)
await page.getByRole("button", { name: /Neuer Eintrag|New Entry/i }).click()
await expect(page).toHaveURL(/\/collections\/content\/entries\/new/)
await expect(page.getByRole("heading", { level: 1, name: /Neuer Eintrag|New Entry/i })).toBeVisible()
}
export { expect, type Page }
+30
View File
@@ -0,0 +1,30 @@
import { test, expect, openNovaProjectDashboard } from "./fixtures"
test.describe("Admin smoke", () => {
test("logs in and shows the core collection groups", async ({ page }) => {
await openNovaProjectDashboard(page)
await expect(page.getByRole("heading", { level: 2, name: "Inhalte" })).toBeVisible()
await expect(page.getByRole("heading", { level: 2, name: "Medien" })).toBeVisible()
await expect(page.getByRole("heading", { level: 2, name: "Struktur" })).toBeVisible()
await expect(page.getByRole("link", { name: /Inhalte/ }).first()).toBeVisible()
await expect(page.getByRole("link", { name: /Mediathek/ }).first()).toBeVisible()
await expect(page.getByRole("link", { name: /Navigation/ }).first()).toBeVisible()
})
test("opens the content collection with the current admin configuration", async ({ page }) => {
await openNovaProjectDashboard(page)
await page
.getByRole("link", { name: /Inhalte/ })
.first()
.click()
await expect(page).toHaveURL(/\/collections\/content$/)
await expect(page.locator("main h1")).toHaveText("Inhalte")
await expect(page.getByRole("button", { name: /Neuer Eintrag|New Entry/i })).toBeVisible()
await expect(page.locator("main")).toContainText("Sprache")
await expect(page.locator("main")).toContainText("Inhaltsblöcke")
})
})
+7
View File
@@ -44,6 +44,13 @@ function loadProjectEnvValue(key: string): string | undefined {
export const ADMIN_TOKEN = process.env.ADMIN_TOKEN || loadAdminTokenFromEnvFile() || "CHANGE_ME" export const ADMIN_TOKEN = process.env.ADMIN_TOKEN || loadAdminTokenFromEnvFile() || "CHANGE_ME"
export const API_BASE = "/api" export const API_BASE = "/api"
export const TEST_BASE_URL = process.env.CODING_URL || loadProjectEnvValue("CODING_URL") || "http://localhost:3000" export const TEST_BASE_URL = process.env.CODING_URL || loadProjectEnvValue("CODING_URL") || "http://localhost:3000"
export const TEST_ADMIN_BASE_URL =
process.env.CODING_TIBIADMIN_URL || loadProjectEnvValue("CODING_TIBIADMIN_URL") || "http://localhost:3000"
export const ADMIN_UI_CREDENTIALS = {
username: process.env.ADMIN_UI_USERNAME || "admin",
password: process.env.ADMIN_UI_PASSWORD || "admin",
} as const
export const SEEDED_TEST_CONTENT = { export const SEEDED_TEST_CONTENT = {
home: { home: {
+3
View File
@@ -5,6 +5,9 @@ import { TEST_BASE_URL } from "./fixtures/test-constants"
async function globalSetup() { async function globalSetup() {
const baseURL = TEST_BASE_URL const baseURL = TEST_BASE_URL
// Seed cleanup/creation stays run-scoped here so all workers consume the
// same deterministic dataset instead of racing on shared test records.
const ctx = await request.newContext({ const ctx = await request.newContext({
baseURL, baseURL,
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
+3
View File
@@ -6,6 +6,9 @@ import { TEST_BASE_URL } from "./fixtures/test-constants"
async function globalTeardown() { async function globalTeardown() {
const baseURL = TEST_BASE_URL const baseURL = TEST_BASE_URL
// Final seed cleanup also stays run-scoped here. Per-test or per-worker
// cleanup would race with parallel workers against the shared seeded data.
try { try {
const probeContext = await import("@playwright/test").then(({ request }) => const probeContext = await import("@playwright/test").then(({ request }) =>
request.newContext({ baseURL, ignoreHTTPSErrors: true }) request.newContext({ baseURL, ignoreHTTPSErrors: true })