mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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.
This commit is contained in:
@@ -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<string> {
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user