diff --git a/.agents/skills/playwright-testing/SKILL.md b/.agents/skills/playwright-testing/SKILL.md index 3173569..dceae88 100644 --- a/.agents/skills/playwright-testing/SKILL.md +++ b/.agents/skills/playwright-testing/SKILL.md @@ -23,6 +23,7 @@ This starter uses Playwright across four slices: - `tests/api/` for API-level checks - `tests/e2e/` for desktop browser behavior +- `tests/e2e-admin/` for committed admin smoke coverage - `tests/e2e-mobile/` for mobile behavior - `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/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-admin/fixtures.ts` | Admin login helpers and admin smoke fixture setup | | `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. +### 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 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. +Use a hidden per-collection marker field as the default seed identity strategy. +In this project the convention is `_testdata: true`. + ### Seed lifecycle 1. `globalSetup` probes the configured base URL. 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. 5. Tests run against those seeded routes. 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 Defined in `tests/fixtures/test-constants.ts`: @@ -173,6 +223,20 @@ Use `tests/e2e/` when validating: - block rendering in the real UI - 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 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. +## 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 @@ -211,6 +300,16 @@ Helpers include: - `clickSpaLink(page, selector)` - 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 Use `tests/e2e-mobile/fixtures.ts`. @@ -265,6 +364,7 @@ Run only the slice you changed. ```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/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 ``` diff --git a/api/collections/content.yml b/api/collections/content.yml index 0f8bb69..8184049 100644 --- a/api/collections/content.yml +++ b/api/collections/content.yml @@ -459,3 +459,7 @@ fields: type: string[] meta: label: { de: "Meta-Schlüsselwörter", en: "Meta Keywords" } + - name: _testdata + type: boolean + meta: + hide: true diff --git a/api/collections/medialib.yml b/api/collections/medialib.yml index 7503bcb..b9e4d70 100644 --- a/api/collections/medialib.yml +++ b/api/collections/medialib.yml @@ -118,3 +118,7 @@ fields: meta: label: { de: "Tags", en: "Tags" } widget: chipArray + - name: _testdata + type: boolean + meta: + hide: true diff --git a/api/collections/navigation.yml b/api/collections/navigation.yml index 5d6b2c5..cab99ab 100644 --- a/api/collections/navigation.yml +++ b/api/collections/navigation.yml @@ -115,3 +115,7 @@ fields: type: object[] meta: label: { de: "Unterpunkte", en: "Child Items" } + - name: _testdata + type: boolean + meta: + hide: true diff --git a/api/collections/ssr.yml b/api/collections/ssr.yml index 09803a6..a835b8c 100644 --- a/api/collections/ssr.yml +++ b/api/collections/ssr.yml @@ -4,58 +4,63 @@ name: ssr meta: - label: { de: "SSR Dummy", en: "ssr dummy" } - muiIcon: server - group: system - hide: true + label: { de: "SSR Dummy", en: "ssr dummy" } + muiIcon: server + group: system + hide: true permissions: - public: - methods: - get: true - post: true - put: false - delete: false - user: - methods: - get: true - post: false - put: false - delete: true + public: + methods: + get: true + post: true + put: false + delete: false + user: + methods: + get: true + post: false + put: false + delete: true hooks: - get: - read: - type: javascript - file: hooks/ssr/get_read.js - post: - bind: - type: javascript - file: hooks/ssr/post_bind.js + get: + read: + type: javascript + file: hooks/ssr/get_read.js + post: + bind: + type: javascript + file: hooks/ssr/post_bind.js fields: - - name: path - type: string - index: [single, unique] + - name: path + type: string + index: [single, unique] - - name: content - type: string - meta: - inputProps: - multiline: true + - name: content + type: string + meta: + inputProps: + multiline: true - - name: validUntil - type: date - index: [single] - meta: - label: - de: Gültig bis - en: Valid until + - name: validUntil + type: date + index: [single] + meta: + label: + de: Gültig bis + en: Valid until - - name: dependencies - type: string[] - index: [single] - meta: - label: - de: Abhängigkeiten - en: Dependencies + - name: dependencies + type: string[] + index: [single] + meta: + label: + de: Abhängigkeiten + en: Dependencies + + - name: _testdata + type: boolean + meta: + hide: true diff --git a/playwright.config.ts b/playwright.config.ts index 6e1ae7d..c1a3550 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,5 +1,5 @@ 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. @@ -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 ────────────────────────────────────────────────── */ { name: "mobile-iphonese", diff --git a/tests/AGENTS.md b/tests/AGENTS.md index b1f0a79..b66eaad 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -9,11 +9,13 @@ For the full current workflow, prefer the `playwright-testing` skill. - All tests: `yarn test` - E2E: `yarn test:e2e` - API: `yarn test:api` +- Admin smoke: `npx playwright test tests/e2e-admin/smoke.spec.ts --project=admin` - Visual regression: `yarn test:visual` - Single file: `npx playwright test tests/e2e/filename.spec.ts` - Single test: `npx playwright test -g "test name"` - 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/...`. +- 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. ## 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. - Seed data lives in `tests/api/helpers/seed-data.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}":`. ## Fixtures & helpers - `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-mobile/fixtures.ts` — Mobile helpers (`openHamburgerMenu`, `isMobileViewport`, `isTabletViewport`, `isBelowLg`). - `api/fixtures.ts` — API fixtures (`api`, `adminApi`). - `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`). +- 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. ## Visual regression diff --git a/tests/api/helpers/seed-data.ts b/tests/api/helpers/seed-data.ts index 2b99f48..22412c8 100644 --- a/tests/api/helpers/seed-data.ts +++ b/tests/api/helpers/seed-data.ts @@ -4,7 +4,9 @@ import { createCollectionEntry, deleteCollectionEntry, listCollectionEntries } f type ContentEntry = { id?: string _id?: string | { $oid?: string } + _testdata?: boolean translationKey?: string + path?: string [key: string]: unknown } @@ -16,9 +18,27 @@ function getEntryId(entry: ContentEntry): string | undefined { } const SEEDED_TRANSLATION_KEYS = new Set(Object.values(SEEDED_TEST_CONTENT).map((entry) => entry.translationKey)) +const SEEDED_PATHS = new Set(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 = [ { + _testdata: true, active: true, type: "page", lang: "de", @@ -99,6 +119,7 @@ const SEEDED_CONTENT_ENTRIES = [ ], }, { + _testdata: true, active: true, type: "page", lang: "en", @@ -179,6 +200,7 @@ const SEEDED_CONTENT_ENTRIES = [ ], }, { + _testdata: true, active: true, type: "page", lang: "de", @@ -208,6 +230,7 @@ const SEEDED_CONTENT_ENTRIES = [ ], }, { + _testdata: true, active: true, type: "page", lang: "en", @@ -237,6 +260,7 @@ const SEEDED_CONTENT_ENTRIES = [ ], }, { + _testdata: true, active: false, type: "page", lang: "de", @@ -265,9 +289,9 @@ const SEEDED_CONTENT_ENTRIES = [ export async function cleanupSeededTestContent(baseURL: string): Promise { const contentEntries = await listCollectionEntries(baseURL, "content") - const seededEntries = contentEntries.filter( - (entry) => typeof entry.translationKey === "string" && SEEDED_TRANSLATION_KEYS.has(entry.translationKey) - ) + // Cleanup runs before every seed pass so leftovers from aborted test runs + // are removed on the next successful global setup. + const seededEntries = contentEntries.filter((entry) => isSeededContentEntry(entry)) let deleted = 0 for (const entry of seededEntries) { diff --git a/tests/e2e-admin/content-config.spec.ts b/tests/e2e-admin/content-config.spec.ts new file mode 100644 index 0000000..faa92f6 --- /dev/null +++ b/tests/e2e-admin/content-config.spec.ts @@ -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() + }) +}) diff --git a/tests/e2e-admin/fixtures.ts b/tests/e2e-admin/fixtures.ts new file mode 100644 index 0000000..b7dc144 --- /dev/null +++ b/tests/e2e-admin/fixtures.ts @@ -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 { + 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 { + 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 { + 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 { + 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 } diff --git a/tests/e2e-admin/smoke.spec.ts b/tests/e2e-admin/smoke.spec.ts new file mode 100644 index 0000000..f72c5c8 --- /dev/null +++ b/tests/e2e-admin/smoke.spec.ts @@ -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") + }) +}) diff --git a/tests/fixtures/test-constants.ts b/tests/fixtures/test-constants.ts index 48a6a4d..f9d34e0 100644 --- a/tests/fixtures/test-constants.ts +++ b/tests/fixtures/test-constants.ts @@ -44,6 +44,13 @@ function loadProjectEnvValue(key: string): string | undefined { export const ADMIN_TOKEN = process.env.ADMIN_TOKEN || loadAdminTokenFromEnvFile() || "CHANGE_ME" export const API_BASE = "/api" 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 = { home: { diff --git a/tests/global-setup.ts b/tests/global-setup.ts index df96169..b41884a 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -5,6 +5,9 @@ import { TEST_BASE_URL } from "./fixtures/test-constants" async function globalSetup() { 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({ baseURL, ignoreHTTPSErrors: true, diff --git a/tests/global-teardown.ts b/tests/global-teardown.ts index d74d34d..b32ad46 100644 --- a/tests/global-teardown.ts +++ b/tests/global-teardown.ts @@ -6,6 +6,9 @@ import { TEST_BASE_URL } from "./fixtures/test-constants" async function globalTeardown() { 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 { const probeContext = await import("@playwright/test").then(({ request }) => request.newContext({ baseURL, ignoreHTTPSErrors: true })