✨ feat: add admin smoke tests and enhance testing documentation with new strategies and configurations
This commit is contained in:
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 }
|
||||||
@@ -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")
|
||||||
|
})
|
||||||
|
})
|
||||||
Vendored
+7
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
Reference in New Issue
Block a user