diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 12c2387..0244588 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -50,6 +50,8 @@ jobs: run: npx playwright test --project=chromium env: CI: true + PLAYWRIGHT_WORKERS: 1 + PLAYWRIGHT_HTML_OPEN: never JWT_SECRET: e2e-test-secret-that-is-long-enough SESSION_SECRET: e2e-test-session-secret-long-enough diff --git a/frontend/e2e/auth.setup.ts b/frontend/e2e/auth.setup.ts index 0c19661..f7a6d03 100644 --- a/frontend/e2e/auth.setup.ts +++ b/frontend/e2e/auth.setup.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { expect, test as setup } from "@playwright/test"; -import { TEST_USER } from "./fixtures"; +import { applyVideoSafetyMode, TEST_USER } from "./fixtures"; const authFile = path.join(import.meta.dirname, ".auth", "user.json"); @@ -33,6 +33,8 @@ function isTokenValid(token: string): boolean { * 4. Log in via the UI. */ setup("authenticate", async ({ page }) => { + await applyVideoSafetyMode(page); + // Create .auth directory if it doesn't exist const authDir = path.dirname(authFile); if (!fs.existsSync(authDir)) { diff --git a/frontend/e2e/fixtures/index.ts b/frontend/e2e/fixtures/index.ts index 7fd71bd..91c7dd2 100644 --- a/frontend/e2e/fixtures/index.ts +++ b/frontend/e2e/fixtures/index.ts @@ -60,6 +60,29 @@ async function setupAuthMeMock(page: Page): Promise { } } +/** + * Reduce visual flashing in recorded videos by forcing a dark first paint and + * disabling most animations/transitions in test mode. + */ +export async function applyVideoSafetyMode(page: Page): Promise { + await page.emulateMedia({ reducedMotion: "reduce", colorScheme: "dark" }); + await page.addInitScript(() => { + const style = document.createElement("style"); + style.id = "pw-video-safety-style"; + style.textContent = ` + html, body { + background: #111111 !important; + color-scheme: dark !important; + } + *, *::before, *::after { + animation: none !important; + transition: none !important; + } + `; + document.documentElement.appendChild(style); + }); +} + /** * Extended test fixture that automatically mocks /auth/me on every page * using user data from the JWT in the stored auth file. @@ -72,6 +95,7 @@ async function setupAuthMeMock(page: Page): Promise { */ export const test = base.extend({ page: async ({ page }, use) => { + await applyVideoSafetyMode(page); await setupAuthMeMock(page); await use(page); }, diff --git a/frontend/package.json b/frontend/package.json index 1446930..438a82b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,8 @@ "test:coverage": "vitest run --coverage", "test:e2e": "rm -rf test-results && playwright test --config=playwright.stable.config.ts", "test:e2e:all": "rm -rf test-results && playwright test --config=playwright.all.config.ts", + "test:e2e:local": "PLAYWRIGHT_HTML_OPEN=never PLAYWRIGHT_WORKERS=4 npm run test:e2e", + "test:e2e:all:local": "PLAYWRIGHT_HTML_OPEN=never PLAYWRIGHT_WORKERS=4 npm run test:e2e:all", "test:e2e:with-video": "npm run test:e2e && npm run test:e2e:video", "test:e2e:all:with-video": "npm run test:e2e:all && npm run test:e2e:video", "test:e2e:video": "find \"$PWD/test-results\" -name video.webm -not -path '*retry*' -print0 | xargs -0 ls -tr > /tmp/e2e-videos.list && if [ -s /tmp/e2e-videos.list ]; then sed \"s/^/file '/\" /tmp/e2e-videos.list | sed \"s/$/'/\" > /tmp/e2e-videos.txt && ffmpeg -y -f concat -safe 0 -i /tmp/e2e-videos.txt -c copy test-results/all-tests.webm; else echo 'No videos found to merge'; fi", diff --git a/frontend/playwright.base.config.ts b/frontend/playwright.base.config.ts index a6ed3d2..7bd035c 100644 --- a/frontend/playwright.base.config.ts +++ b/frontend/playwright.base.config.ts @@ -6,6 +6,8 @@ export function buildPlaywrightConfig(runAllBrowsers: boolean) { ? ((globalThis as { process?: { env?: Record } }).process?.env ?? {}) : {}; const baseURL = env.PLAYWRIGHT_BASE_URL || "http://localhost:5173"; + const parsedWorkers = Number.parseInt(env.PLAYWRIGHT_WORKERS ?? "", 10); + const workers = Number.isFinite(parsedWorkers) && parsedWorkers > 0 ? parsedWorkers : env.CI ? 1 : 4; const projects: NonNullable = [ { @@ -64,7 +66,7 @@ export function buildPlaywrightConfig(runAllBrowsers: boolean) { fullyParallel: true, forbidOnly: !!env.CI, retries: env.CI ? 2 : 0, - workers: 1, + workers, reporter: env.CI ? [["html", { outputFolder: "playwright-report" }], ["github"]] : [["html", { outputFolder: "playwright-report" }], ["list"]], diff --git a/frontend/src/components/MedDetailModal.tsx b/frontend/src/components/MedDetailModal.tsx index 8639f21..5b3472b 100644 --- a/frontend/src/components/MedDetailModal.tsx +++ b/frontend/src/components/MedDetailModal.tsx @@ -672,7 +672,9 @@ export function MedDetailModal({

{getMedDisplayName(selectedMed)}

- {selectedMed.name && selectedMed.genericName && {selectedMed.genericName}} + {selectedMed.name && selectedMed.genericName && ( + {selectedMed.genericName} + )} {selectedMed.takenBy && (selectedMed.takenBy || []).length > 0 && ( {t("modal.for")}{" "} @@ -1017,7 +1019,11 @@ export function MedDetailModal({ {/* Image Lightbox */} {showImageLightbox && selectedMed.imageUrl && ( - + )} {/* Refill Modal */} diff --git a/frontend/src/components/MobileEditModal.tsx b/frontend/src/components/MobileEditModal.tsx index 380f860..97f5682 100644 --- a/frontend/src/components/MobileEditModal.tsx +++ b/frontend/src/components/MobileEditModal.tsx @@ -253,7 +253,10 @@ export function MobileEditModal({ const mobileTitle = (() => { if (!editingId) return t("form.newEntry"); if (readOnlyMode) return t("form.viewEntry"); - const medicationName = (currentMed ? (currentMed.name?.trim() || currentMed.genericName?.trim()) : null) || form.name.trim() || form.genericName.trim(); + const medicationName = + (currentMed ? currentMed.name?.trim() || currentMed.genericName?.trim() : null) || + form.name.trim() || + form.genericName.trim(); if (!medicationName) return t("form.editEntry"); return t("form.editEntryWithName", { name: medicationName }); })(); @@ -366,7 +369,9 @@ export function MobileEditModal({ {fieldErrors.name} )} -