From 9724e5229c903c661e6d91425b5bf14068810ccb Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Thu, 23 Apr 2026 09:59:41 +0800 Subject: [PATCH] test: add comprehensive E2E Playwright tests for analytics feature Covers consent flow, API endpoints, privacy/no-data-leak verification, disabled-server behavior, settings toggle, and privacy policy page. --- tests/e2e-docker/analytics-api.spec.ts | 191 ++++++++++++++++ tests/e2e-docker/analytics-consent.spec.ts | 124 ++++++++++ tests/e2e-docker/analytics-disabled.spec.ts | 88 +++++++ .../e2e-docker/analytics-no-data-leak.spec.ts | 215 ++++++++++++++++++ .../analytics-privacy-policy.spec.ts | 61 +++++ 5 files changed, 679 insertions(+) create mode 100644 tests/e2e-docker/analytics-api.spec.ts create mode 100644 tests/e2e-docker/analytics-consent.spec.ts create mode 100644 tests/e2e-docker/analytics-disabled.spec.ts create mode 100644 tests/e2e-docker/analytics-no-data-leak.spec.ts create mode 100644 tests/e2e-docker/analytics-privacy-policy.spec.ts diff --git a/tests/e2e-docker/analytics-api.spec.ts b/tests/e2e-docker/analytics-api.spec.ts new file mode 100644 index 00000000..35344497 --- /dev/null +++ b/tests/e2e-docker/analytics-api.spec.ts @@ -0,0 +1,191 @@ +import { expect, test } from "@playwright/test"; + +// ─── Analytics API Endpoints ──────────────────────────────────────── +// Tests for the analytics config and user consent API endpoints. +// These run against the Docker container at localhost:1349. + +const BASE_URL = "http://localhost:1349"; +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** Login and return a Bearer token for authenticated requests. */ +async function getAuthToken(): Promise { + const res = await fetch(`${BASE_URL}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: "admin", password: "admin" }), + }); + const data = await res.json(); + return data.token; +} + +test.describe("GET /api/v1/config/analytics (public)", () => { + test("returns 200 without auth token", async () => { + const res = await fetch(`${BASE_URL}/api/v1/config/analytics`); + expect(res.status).toBe(200); + }); + + test("response has correct analytics config shape", async () => { + const res = await fetch(`${BASE_URL}/api/v1/config/analytics`); + const config = await res.json(); + + expect(config).toHaveProperty("enabled"); + expect(config).toHaveProperty("posthogApiKey"); + expect(config).toHaveProperty("posthogHost"); + expect(config).toHaveProperty("sentryDsn"); + expect(config).toHaveProperty("sampleRate"); + expect(config).toHaveProperty("instanceId"); + + expect(typeof config.enabled).toBe("boolean"); + expect(typeof config.posthogApiKey).toBe("string"); + expect(typeof config.posthogHost).toBe("string"); + expect(typeof config.sentryDsn).toBe("string"); + expect(typeof config.sampleRate).toBe("number"); + expect(typeof config.instanceId).toBe("string"); + }); + + test("instanceId is a valid UUID when analytics enabled", async () => { + const res = await fetch(`${BASE_URL}/api/v1/config/analytics`); + const config = await res.json(); + + if (config.enabled) { + expect(config.instanceId).toMatch(UUID_REGEX); + } else { + // When disabled, instanceId is empty string + expect(config.instanceId).toBe(""); + } + }); + + test("instanceId is consistent across multiple fetches", async () => { + const res1 = await fetch(`${BASE_URL}/api/v1/config/analytics`); + const config1 = await res1.json(); + + const res2 = await fetch(`${BASE_URL}/api/v1/config/analytics`); + const config2 = await res2.json(); + + expect(config1.instanceId).toBe(config2.instanceId); + }); +}); + +test.describe("PUT /api/v1/user/analytics (auth required)", () => { + test("returns 401 without auth token", async () => { + const res = await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: true }), + }); + expect(res.status).toBe(401); + }); + + test("accepts consent with enabled: true", async () => { + const token = await getAuthToken(); + + const res = await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ enabled: true }), + }); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body).toEqual({ ok: true, analyticsEnabled: true }); + }); + + test("declines consent with enabled: false", async () => { + const token = await getAuthToken(); + + const res = await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ enabled: false }), + }); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body).toEqual({ ok: true, analyticsEnabled: false }); + }); + + test("remind later sets analyticsEnabled to null", async () => { + const token = await getAuthToken(); + + const res = await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ remindLater: true }), + }); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body).toEqual({ ok: true, analyticsEnabled: null }); + }); +}); + +test.describe("GET /api/auth/session includes analytics fields", () => { + test("session response contains analytics consent fields", async () => { + const token = await getAuthToken(); + + // First set a consent preference so the fields are populated + await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ enabled: true }), + }); + + // Fetch session + const sessionRes = await fetch(`${BASE_URL}/api/auth/session`, { + headers: { Authorization: `Bearer ${token}` }, + }); + expect(sessionRes.status).toBe(200); + + const session = await sessionRes.json(); + + // The user object should include analytics fields + expect(session.user).toHaveProperty("analyticsEnabled"); + expect(session.user).toHaveProperty("analyticsConsentShownAt"); + expect(session.user).toHaveProperty("analyticsConsentRemindAt"); + + // After accepting, analyticsEnabled should be true + expect(session.user.analyticsEnabled).toBe(true); + // analyticsConsentShownAt should be a timestamp (number) + expect(typeof session.user.analyticsConsentShownAt).toBe("number"); + // analyticsConsentRemindAt should be null after explicit accept + expect(session.user.analyticsConsentRemindAt).toBeNull(); + }); + + test("session reflects remind-later state", async () => { + const token = await getAuthToken(); + + // Set remind later + await fetch(`${BASE_URL}/api/v1/user/analytics`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ remindLater: true }), + }); + + // Fetch session + const sessionRes = await fetch(`${BASE_URL}/api/auth/session`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const session = await sessionRes.json(); + + expect(session.user.analyticsEnabled).toBeNull(); + expect(typeof session.user.analyticsConsentShownAt).toBe("number"); + // analyticsConsentRemindAt should be a future timestamp + expect(typeof session.user.analyticsConsentRemindAt).toBe("number"); + expect(session.user.analyticsConsentRemindAt).toBeGreaterThan(Date.now()); + }); +}); diff --git a/tests/e2e-docker/analytics-consent.spec.ts b/tests/e2e-docker/analytics-consent.spec.ts new file mode 100644 index 00000000..b204f404 --- /dev/null +++ b/tests/e2e-docker/analytics-consent.spec.ts @@ -0,0 +1,124 @@ +import { expect, test } from "@playwright/test"; + +// ─── Analytics Consent Page Flow ──────────────────────────────────── +// These tests run against a Docker container at localhost:1349. +// The container must be started with SKIP_MUST_CHANGE_PASSWORD=true. +// The analytics consent screen still appears after login for fresh users. + +/** + * Helper: login with admin/admin and return the page (no saved auth state). + * This gives us a "fresh" session where the consent screen hasn't been dismissed. + */ +async function loginFresh(page: import("@playwright/test").Page) { + await page.goto("/login"); + await page.getByLabel("Username").fill("admin"); + await page.getByLabel("Password").fill("admin"); + await page.getByRole("button", { name: /login/i }).click(); +} + +test.describe("Analytics consent page", () => { + // Use a fresh browser context per test — no saved auth state + test.use({ storageState: { cookies: [], origins: [] } }); + + test("consent page appears for fresh users after login", async ({ page }) => { + await loginFresh(page); + + // After login, the AuthGuard should redirect to /analytics-consent + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + await expect(page).toHaveURL("/analytics-consent"); + + // Verify consent page content + await expect(page.getByText("Make ashim better for you")).toBeVisible({ timeout: 10_000 }); + + // Shield icon is rendered via Lucide — verify it exists as an SVG + const shieldIcon = page.locator("svg.lucide-shield"); + await expect(shieldIcon).toBeVisible({ timeout: 5_000 }); + + // Both action buttons should be visible + await expect(page.getByRole("button", { name: "Sure, sounds good" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Maybe later" })).toBeVisible(); + + // Verify the two columns + await expect(page.getByText("What's shared:")).toBeVisible(); + await expect(page.getByText("What NEVER leaves your machine:")).toBeVisible(); + }); + + test("accept analytics redirects to home and does not show consent again", async ({ page }) => { + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + + // Click accept + await page.getByRole("button", { name: "Sure, sounds good" }).click(); + + // Should redirect to home + await page.waitForURL("/", { timeout: 15_000 }); + await expect(page).toHaveURL("/"); + + // Navigate away and back — consent page should NOT reappear + await page.goto("/resize"); + await page.waitForTimeout(2_000); + await page.goto("/"); + await page.waitForTimeout(2_000); + await expect(page).toHaveURL("/"); + await expect(page).not.toHaveURL(/analytics-consent/); + }); + + test("decline (Maybe later) redirects to home and does not show consent again immediately", async ({ + page, + }) => { + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + + // Click decline + await page.getByRole("button", { name: "Maybe later" }).click(); + + // Should redirect to home + await page.waitForURL("/", { timeout: 15_000 }); + await expect(page).toHaveURL("/"); + + // Navigate away and back — consent page should NOT reappear (until 7 days) + await page.goto("/resize"); + await page.waitForTimeout(2_000); + await page.goto("/"); + await page.waitForTimeout(2_000); + await expect(page).toHaveURL("/"); + await expect(page).not.toHaveURL(/analytics-consent/); + }); + + test("settings toggle works after accepting analytics", async ({ page }) => { + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + + // Accept analytics first + await page.getByRole("button", { name: "Sure, sounds good" }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + // Open Settings dialog + const settingsButton = page + .getByRole("button", { name: /settings/i }) + .or(page.locator("button[aria-label*='ettings']")); + await expect(settingsButton).toBeVisible({ timeout: 10_000 }); + await settingsButton.click(); + + // Navigate to Product Analytics section in the settings nav + const analyticsNav = page.getByText("Product Analytics"); + await expect(analyticsNav).toBeVisible({ timeout: 5_000 }); + await analyticsNav.click(); + + // Verify toggle shows enabled state + await expect(page.getByText("Analytics enabled")).toBeVisible({ timeout: 5_000 }); + + // Find and click the toggle button to disable + const toggleButton = page.locator( + "button.rounded-full[class*='bg-primary'], button[class*='rounded-full'][class*='bg-']", + ); + await toggleButton.click(); + + // Verify it now shows disabled + await expect(page.getByText("Analytics disabled")).toBeVisible({ timeout: 5_000 }); + + // Toggle back on + await toggleButton.click(); + await expect(page.getByText("Analytics enabled")).toBeVisible({ timeout: 5_000 }); + }); +}); diff --git a/tests/e2e-docker/analytics-disabled.spec.ts b/tests/e2e-docker/analytics-disabled.spec.ts new file mode 100644 index 00000000..53687332 --- /dev/null +++ b/tests/e2e-docker/analytics-disabled.spec.ts @@ -0,0 +1,88 @@ +import { expect, test } from "@playwright/test"; + +// ─── Analytics Disabled (ANALYTICS_ENABLED=false) ─────────────────── +// These tests verify behavior when the server has ANALYTICS_ENABLED=false. +// They need a Docker container started with ANALYTICS_ENABLED=false. +// +// If the container has analytics enabled, these tests will be skipped +// automatically by checking the config endpoint first. + +const BASE_URL = "http://localhost:1349"; + +async function loginFresh(page: import("@playwright/test").Page) { + await page.goto("/login"); + await page.getByLabel("Username").fill("admin"); + await page.getByLabel("Password").fill("admin"); + await page.getByRole("button", { name: /login/i }).click(); +} + +test.describe("Analytics disabled by server", () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test.beforeEach(async ({ request: _request }, testInfo) => { + // Skip this suite if the container has analytics enabled + const res = await fetch(`${BASE_URL}/api/v1/config/analytics`); + const config = await res.json(); + if (config.enabled) { + testInfo.skip(); + } + }); + + test("config endpoint returns disabled with empty fields", async ({ request }) => { + const res = await request.get("/api/v1/config/analytics"); + expect(res.ok()).toBeTruthy(); + + const config = await res.json(); + expect(config).toEqual({ + enabled: false, + posthogApiKey: "", + posthogHost: "", + sentryDsn: "", + sampleRate: 0, + instanceId: "", + }); + }); + + test("no consent screen when analytics disabled — goes directly to home", async ({ page }) => { + await loginFresh(page); + + // Should go directly to home, NOT to /analytics-consent + await page.waitForURL("/", { timeout: 30_000 }); + await expect(page).toHaveURL("/"); + }); + + test("no outbound network requests to PostHog or Sentry", async ({ page }) => { + const analyticsRequests: string[] = []; + + // Intercept ALL network requests and log any that hit analytics domains + await page.route("**/*", (route) => { + const url = route.request().url(); + if ( + url.includes("posthog.com") || + url.includes("posthog") || + url.includes("sentry.io") || + url.includes("sentry") || + url.includes("us.i.posthog.com") || + url.includes("ingest.sentry.io") + ) { + analyticsRequests.push(url); + } + return route.continue(); + }); + + // Login + await loginFresh(page); + await page.waitForURL("/", { timeout: 30_000 }); + + // Navigate around + await page.goto("/resize"); + await page.waitForTimeout(2_000); + await page.goto("/compress"); + await page.waitForTimeout(2_000); + await page.goto("/"); + await page.waitForTimeout(2_000); + + // Assert ZERO analytics requests + expect(analyticsRequests).toEqual([]); + }); +}); diff --git a/tests/e2e-docker/analytics-no-data-leak.spec.ts b/tests/e2e-docker/analytics-no-data-leak.spec.ts new file mode 100644 index 00000000..1ed4570c --- /dev/null +++ b/tests/e2e-docker/analytics-no-data-leak.spec.ts @@ -0,0 +1,215 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +// ─── Analytics Privacy / No Data Leak ─────────────────────────────── +// CRITICAL privacy tests: verify that declining analytics means +// absolutely zero data is sent to PostHog, Sentry, or any external +// analytics domain. Also verifies that tool functionality is not +// degraded when analytics are declined. + +const SAMPLES_DIR = path.join(process.env.HOME ?? "/Users/sidd", "Downloads", "sample"); +const FIXTURES_DIR = path.join(process.cwd(), "tests", "fixtures"); + +/** Analytics-related domains to watch for in network traffic. */ +const ANALYTICS_DOMAINS = [ + "posthog.com", + "us.i.posthog.com", + "eu.i.posthog.com", + "sentry.io", + "ingest.sentry.io", + "o4508.ingest.us.sentry.io", +]; + +function isAnalyticsRequest(url: string): boolean { + return ANALYTICS_DOMAINS.some((domain) => url.includes(domain)); +} + +async function loginFresh(page: import("@playwright/test").Page) { + await page.goto("/login"); + await page.getByLabel("Username").fill("admin"); + await page.getByLabel("Password").fill("admin"); + await page.getByRole("button", { name: /login/i }).click(); +} + +function getFixture(name: string): string { + return path.join(FIXTURES_DIR, name); +} + +async function uploadFiles( + page: import("@playwright/test").Page, + filePaths: string[], +): Promise { + const fileChooserPromise = page.waitForEvent("filechooser"); + const dropzone = page.locator("[class*='border-dashed']").first(); + await dropzone.click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(filePaths); + await page.waitForTimeout(3_000); +} + +async function waitForProcessingDone( + page: import("@playwright/test").Page, + timeoutMs = 60_000, +): Promise { + try { + const spinner = page.locator("[class*='animate-spin']"); + if (await spinner.isVisible({ timeout: 3_000 })) { + await spinner.waitFor({ state: "hidden", timeout: timeoutMs }); + } + } catch { + // No spinner — processing may have been instant + } + await page.waitForTimeout(500); +} + +test.describe("No data leak after declining analytics", () => { + // Use fresh browser context — no saved auth state + test.use({ storageState: { cookies: [], origins: [] } }); + + test("zero PostHog/Sentry traffic after explicitly declining analytics", async ({ page }) => { + const analyticsRequests: string[] = []; + + // Set up network interception BEFORE any navigation + await page.route("**/*", (route) => { + const url = route.request().url(); + if (isAnalyticsRequest(url)) { + analyticsRequests.push(url); + } + return route.continue(); + }); + + // Login and decline analytics + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + await page.getByRole("button", { name: "Maybe later" }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + // Navigate to several pages + await page.goto("/resize"); + await page.waitForTimeout(2_000); + await page.goto("/compress"); + await page.waitForTimeout(2_000); + await page.goto("/fullscreen"); + await page.waitForTimeout(2_000); + await page.goto("/"); + await page.waitForTimeout(2_000); + + // Assert ZERO analytics requests were made + expect( + analyticsRequests, + `Expected zero analytics requests, but found: ${analyticsRequests.join(", ")}`, + ).toEqual([]); + }); + + test("zero PostHog/Sentry traffic for fresh user who chose Maybe later", async ({ page }) => { + const analyticsRequests: string[] = []; + + await page.route("**/*", (route) => { + const url = route.request().url(); + if (isAnalyticsRequest(url)) { + analyticsRequests.push(url); + } + return route.continue(); + }); + + // Login and immediately dismiss via "Maybe later" + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + await page.getByRole("button", { name: "Maybe later" }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + // Browse around + await page.goto("/crop"); + await page.waitForTimeout(2_000); + await page.goto("/convert"); + await page.waitForTimeout(2_000); + await page.goto("/"); + await page.waitForTimeout(2_000); + + expect( + analyticsRequests, + `Expected zero analytics requests, but found: ${analyticsRequests.join(", ")}`, + ).toEqual([]); + }); + + test("tool processing works normally after declining analytics", async ({ page }) => { + // Login and decline analytics + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + await page.getByRole("button", { name: "Maybe later" }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + // Navigate to resize tool + await page.goto("/resize"); + await page.waitForTimeout(2_000); + + // Upload a test image + const testImage = getFixture("test-200x150.png"); + await uploadFiles(page, [testImage]); + + // Set resize parameters + const widthInput = page.getByLabel("Width (px)"); + await widthInput.fill("100"); + + // Process the image + const processBtn = page.getByTestId("resize-submit"); + await expect(processBtn).toBeEnabled({ timeout: 15_000 }); + await processBtn.click(); + await waitForProcessingDone(page); + + // Verify no errors + const error = page.locator(".text-red-500"); + expect(await error.isVisible({ timeout: 2_000 }).catch(() => false)).toBe(false); + + // Verify a download link or result appeared + const downloadLink = page.locator( + "a[download], a[href*='download'], button:has-text('Download')", + ); + await expect(downloadLink.first()).toBeVisible({ timeout: 15_000 }); + }); + + test("tool processing works with sample portrait after declining analytics", async ({ page }) => { + const portraitPath = path.join( + SAMPLES_DIR, + "portrait-of-a-smiling-man-with-glasses-and-a-beard-isolated.png", + ); + if (!fs.existsSync(portraitPath)) { + test.skip(); + return; + } + + // Login and decline analytics + await loginFresh(page); + await page.waitForURL("**/analytics-consent", { timeout: 30_000 }); + await page.getByRole("button", { name: "Maybe later" }).click(); + await page.waitForURL("/", { timeout: 15_000 }); + + // Navigate to resize tool + await page.goto("/resize"); + await page.waitForTimeout(2_000); + + // Upload sample portrait + await uploadFiles(page, [portraitPath]); + + // Set resize width + const widthInput = page.getByLabel("Width (px)"); + await widthInput.fill("200"); + + // Process + const processBtn = page.getByTestId("resize-submit"); + await expect(processBtn).toBeEnabled({ timeout: 15_000 }); + await processBtn.click(); + await waitForProcessingDone(page); + + // Verify no errors + const error = page.locator(".text-red-500"); + expect(await error.isVisible({ timeout: 2_000 }).catch(() => false)).toBe(false); + + // Verify download appeared — proves functionality is not degraded + const downloadLink = page.locator( + "a[download], a[href*='download'], button:has-text('Download')", + ); + await expect(downloadLink.first()).toBeVisible({ timeout: 15_000 }); + }); +}); diff --git a/tests/e2e-docker/analytics-privacy-policy.spec.ts b/tests/e2e-docker/analytics-privacy-policy.spec.ts new file mode 100644 index 00000000..c173d434 --- /dev/null +++ b/tests/e2e-docker/analytics-privacy-policy.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; + +// ─── Privacy Policy Page ──────────────────────────────────────────── +// Tests for the /privacy page content, verifying that the updated +// privacy policy reflects the analytics feature accurately. + +test.describe("Privacy policy page", () => { + test("privacy policy page is accessible and loads", async ({ page }) => { + await page.goto("/privacy"); + + // Verify the page loads with the correct title + await expect(page.getByRole("heading", { name: "Privacy Policy" })).toBeVisible({ + timeout: 10_000, + }); + }); + + test("Product Analytics section exists", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByRole("heading", { name: "Product Analytics" })).toBeVisible({ + timeout: 10_000, + }); + }); + + test("Your Choice section exists", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByRole("heading", { name: "Your Choice" })).toBeVisible({ + timeout: 10_000, + }); + }); + + test("Third-Party Services section exists", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByRole("heading", { name: "Third-Party Services" })).toBeVisible({ + timeout: 10_000, + }); + }); + + test("mentions PostHog and Sentry by name", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByText("PostHog")).toBeVisible({ timeout: 10_000 }); + await expect(page.getByText("Sentry")).toBeVisible({ timeout: 10_000 }); + }); + + test("shows correct last-updated date", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByText("April 22, 2026")).toBeVisible({ timeout: 10_000 }); + }); + + test("does NOT contain old 'No Tracking or Analytics' text", async ({ page }) => { + await page.goto("/privacy"); + + // The old privacy policy had this text — it should be gone now + const oldText = page.getByText("No Tracking or Analytics"); + await expect(oldText).not.toBeVisible({ timeout: 5_000 }); + }); + + test("back-to-app link is present", async ({ page }) => { + await page.goto("/privacy"); + await expect(page.getByText("Back to app")).toBeVisible({ timeout: 10_000 }); + }); +});