fix: E2E test isolation and analytics consent bugs

- Isolate test web server on port 2349 so E2E tests run alongside Docker
- Use fresh SQLite database per test run via DB_PATH, fixing missing
  roles/audit_log tables from stale migration state
- Fix analytics consent redirect loop when ANALYTICS_ENABLED=false by
  auto-declining consent instead of bare redirect
- Dismiss analytics consent via API in auth setup and RBAC test user
  creation so the consent page never blocks UI tests
- Make Vite port and proxy target configurable via env vars
This commit is contained in:
ashim-hq
2026-04-24 01:24:15 +08:00
parent fe384ddfe1
commit a062aa9e5f
6 changed files with 72 additions and 11 deletions
@@ -8,7 +8,8 @@ const t = en.analytics;
export function AnalyticsConsentPage() {
const navigate = useNavigate();
const { config, configLoaded, fetchConfig, acceptAnalytics, remindLater } = useAnalyticsStore();
const { config, configLoaded, fetchConfig, acceptAnalytics, declineAnalytics, remindLater } =
useAnalyticsStore();
useEffect(() => {
fetchConfig();
@@ -16,9 +17,9 @@ export function AnalyticsConsentPage() {
useEffect(() => {
if (configLoaded && !config?.enabled) {
navigate("/", { replace: true });
declineAnalytics().then(() => navigate("/", { replace: true }));
}
}, [configLoaded, config, navigate]);
}, [configLoaded, config, navigate, declineAnalytics]);
if (!configLoaded) {
return (
+2 -2
View File
@@ -12,9 +12,9 @@ export default defineConfig({
dedupe: ["react", "react-dom"],
},
server: {
port: 1349,
port: Number(process.env.PORT) || 1349,
proxy: {
"/api": "http://localhost:13490",
"/api": process.env.VITE_API_URL || "http://localhost:13490",
},
},
build: {
+12 -4
View File
@@ -2,6 +2,9 @@ import path from "node:path";
import { defineConfig, devices } from "@playwright/test";
const authFile = path.join(__dirname, "test-results", ".auth", "user.json");
const testDbPath = path.join(__dirname, "test-results", ".e2e-db", "ashim.db");
const TEST_WEB_PORT = 2349;
export default defineConfig({
testDir: "./tests/e2e",
@@ -20,7 +23,7 @@ export default defineConfig({
workers: 1,
reporter: "html",
use: {
baseURL: "http://localhost:1349",
baseURL: `http://localhost:${TEST_WEB_PORT}`,
screenshot: "only-on-failure",
trace: "retain-on-failure",
},
@@ -40,23 +43,28 @@ export default defineConfig({
],
webServer: [
{
command: "pnpm --filter @ashim/api dev",
command: `rm -f "${testDbPath}" "${testDbPath}-shm" "${testDbPath}-wal" && mkdir -p "${path.dirname(testDbPath)}" && pnpm --filter @ashim/api dev`,
port: 13490,
reuseExistingServer: !process.env.CI,
env: {
PORT: "13490",
AUTH_ENABLED: "true",
DEFAULT_USERNAME: "admin",
DEFAULT_PASSWORD: "admin",
RATE_LIMIT_PER_MIN: "50000",
SKIP_MUST_CHANGE_PASSWORD: "true",
ANALYTICS_ENABLED: "false",
DB_PATH: testDbPath,
},
timeout: 30_000,
},
{
command: "pnpm --filter @ashim/web dev",
port: 1349,
port: TEST_WEB_PORT,
reuseExistingServer: !process.env.CI,
env: {
PORT: String(TEST_WEB_PORT),
VITE_API_URL: "http://localhost:13490",
},
timeout: 30_000,
},
],
+15 -2
View File
@@ -14,8 +14,21 @@ setup("authenticate", async ({ page }) => {
await page.getByLabel("Password").fill("admin");
await page.getByRole("button", { name: /login/i }).click();
// Wait for the full-page redirect to "/"
await page.waitForURL("/", { timeout: 15_000 });
// Wait for login to complete and grab the token in one step
const handle = await page.waitForFunction(() => localStorage.getItem("ashim-token"), null, {
timeout: 15_000,
});
const token = await handle.jsonValue();
// Dismiss analytics consent via API so it won't block any test
const apiBase = process.env.API_URL || "http://localhost:13490";
await page.request.put(`${apiBase}/api/v1/user/analytics`, {
headers: { Authorization: `Bearer ${token}` },
data: { enabled: false },
});
// Now navigate to "/" - consent guard is satisfied
await page.goto("/");
await expect(page).toHaveURL("/");
// Save storage state (includes localStorage with the token)
+13
View File
@@ -85,6 +85,19 @@ async function createUserWithRole(
if (!changeRes.ok) {
throw new Error(`Failed to clear mustChangePassword for ${username}: ${changeRes.status}`);
}
// Re-login (change-password invalidates sessions) and dismiss analytics consent
const reLogin = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const reLoginData = await reLogin.json();
await fetch(`${API}/api/v1/user/analytics`, {
method: "PUT",
headers: authJson(reLoginData.token),
body: JSON.stringify({ enabled: false }),
});
}
/** Delete a user by username if it exists. */
+26
View File
@@ -68,6 +68,19 @@ async function ensureTestUser(adminToken: string): Promise<void> {
if (!changeRes.ok) {
throw new Error(`Failed to clear mustChangePassword: ${changeRes.status}`);
}
// Re-login (change-password invalidates sessions) and dismiss analytics consent
const reLogin = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: TEST_USER, password: TEST_PASSWORD }),
});
const reLoginData = await reLogin.json();
await fetch(`${API}/api/v1/user/analytics`, {
method: "PUT",
headers: authJson(reLoginData.token),
body: JSON.stringify({ enabled: false }),
});
}
/** Delete the test user if it exists. */
@@ -204,6 +217,19 @@ base.describe("RBAC - Editor sees collaborative tabs", () => {
newPassword: "EditorTest1",
}),
});
// Re-login and dismiss analytics consent
const reLogin = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "editortest", password: "EditorTest1" }),
});
const reLoginData = await reLogin.json();
await fetch(`${API}/api/v1/user/analytics`, {
method: "PUT",
headers: authJson(reLoginData.token),
body: JSON.stringify({ enabled: false }),
});
});
base.afterAll(async () => {