import fs from "node:fs"; import path from "node:path"; import { performance } from "node:perf_hooks"; import { expect, test, type Route } from "@playwright/test"; const targetTenantId = process.env.TENANT_PROFILE_PERF_TENANT_ID ?? "56cd0fd7-b62a-43c0-8db9-74a30468d7cb"; const evidenceDir = path.resolve("e2e-evidence"); type ApiTiming = { method: string; url: string; status: number; durationMs: number; }; type Measurement = { sample: number; configFieldsVisibleMs: number; networkIdleMs: number; orgUnitType: string | null; visibility: string | null; worksmobileSync: string | null; apiTimings: ApiTiming[]; }; async function fulfillFromLocalApi(route: Route, targetUrl?: string) { const request = route.request(); const corsHeaders = { "access-control-allow-headers": "authorization,content-type,x-test-role", "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS", "access-control-allow-origin": "*", }; if (request.method() === "OPTIONS") { await route.fulfill({ status: 204, headers: corsHeaders }); return; } const headers = { ...request.headers(), "x-test-role": "super_admin" }; delete headers.authorization; delete headers.host; const response = await route.fetch({ url: targetUrl, headers }); await route.fulfill({ response, headers: { ...response.headers(), ...corsHeaders }, }); } function resolveActualApiBaseUrl() { const explicitApiBaseUrl = process.env.TENANT_PROFILE_PERF_API_BASE_URL; if (explicitApiBaseUrl?.trim()) { return explicitApiBaseUrl.trim().replace(/\/$/, ""); } const proxyTarget = process.env.API_PROXY_TARGET; if (proxyTarget?.trim()) { return new URL("/api", `${proxyTarget.trim().replace(/\/$/, "")}/`) .toString() .replace(/\/$/, ""); } return "http://127.0.0.1:5173/api"; } async function canFetchJsonFromLocalApi(apiBaseUrl: string) { const probeUrl = `${apiBaseUrl.replace(/\/$/, "")}/v1/user/me`; try { const response = await fetch(probeUrl, { headers: { "x-test-role": "super_admin" }, }); const contentType = response.headers.get("content-type") ?? ""; return contentType.toLowerCase().includes("application/json"); } catch { return false; } } function percentile(values: number[], ratio: number) { const sorted = [...values].sort((left, right) => left - right); const index = Math.min( sorted.length - 1, Math.ceil(sorted.length * ratio) - 1, ); return sorted[index] ?? 0; } test.describe("Tenant profile local performance evidence", () => { test("loads org config fields through the local API within 500ms", async ({ page, }, testInfo) => { const actualApiBaseUrl = resolveActualApiBaseUrl(); test.skip( !(await canFetchJsonFromLocalApi(actualApiBaseUrl)), `Local API is not available at ${actualApiBaseUrl}; set TENANT_PROFILE_PERF_API_BASE_URL to run this evidence test.`, ); const normalizedActualApiBaseUrl = actualApiBaseUrl.replace(/\/$/, ""); fs.mkdirSync(evidenceDir, { recursive: true }); await page.setViewportSize({ width: 1440, height: 900 }); await page.addInitScript(() => { window.localStorage.setItem("locale", "ko"); window.localStorage.setItem("X-Mock-Role-Enabled", "true"); window.localStorage.setItem("X-Mock-Role", "super_admin"); window.localStorage.removeItem("admin_session"); for (const key of Object.keys(window.localStorage)) { if (key.startsWith("oidc.user:")) { window.localStorage.removeItem(key); } } ( window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean } )._IS_TEST_MODE = true; }); await page.route("**/oidc/**", async (route) => { await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } }); }); await page.route("**/api/**", async (route) => { await fulfillFromLocalApi(route); }); await page.route("http://playwright-mock/api/**", async (route) => { const request = route.request(); const source = new URL(request.url()); const target = `${normalizedActualApiBaseUrl}${source.pathname.replace( /^\/api/, "", )}${source.search}`; await fulfillFromLocalApi(route, target); }); const requestStartedAt = new Map(); const apiTimings: ApiTiming[] = []; page.on("request", (request) => { const url = request.url(); if (url.includes("/api/v1/") || url.includes("playwright-mock/api")) { requestStartedAt.set(request.url(), performance.now()); } }); page.on("response", (response) => { const request = response.request(); const startedAt = requestStartedAt.get(request.url()); if (startedAt === undefined) { return; } const timing = { method: request.method(), url: response.url(), status: response.status(), durationMs: Math.round(performance.now() - startedAt), }; apiTimings.push(timing); }); page.on("requestfailed", (request) => { const url = request.url(); if (url.includes("/api/v1/") || url.includes("playwright-mock/api")) { console.log( "api-request-failed", JSON.stringify({ method: request.method(), url, failure: request.failure()?.errorText, }), ); } }); const measurements: Measurement[] = []; const sampleCount = 5; for (let sample = 1; sample <= sampleCount; sample += 1) { apiTimings.length = 0; const startedAt = performance.now(); await page.goto(`/tenants/${targetTenantId}`, { waitUntil: "domcontentloaded", }); const orgUnitTypeSelect = page.getByTestId("tenant-org-unit-type-select"); await expect(orgUnitTypeSelect).toBeVisible({ timeout: 15000 }); await expect(page.locator("#tenant-visibility")).toBeVisible(); await expect(page.locator("#worksmobileExcluded")).toBeVisible(); const configFieldsVisibleMs = Math.round(performance.now() - startedAt); await page.waitForLoadState("networkidle", { timeout: 15000 }); const networkIdleMs = Math.round(performance.now() - startedAt); measurements.push({ sample, configFieldsVisibleMs, networkIdleMs, orgUnitType: await orgUnitTypeSelect.inputValue(), visibility: await page.locator("#tenant-visibility").inputValue(), worksmobileSync: await page .locator("#worksmobileExcluded") .inputValue(), apiTimings: [...apiTimings], }); } const screenshotPath = path.join( evidenceDir, "tenant-profile-performance-local.png", ); await page.screenshot({ path: screenshotPath, fullPage: true }); const configTimes = measurements.map( (measurement) => measurement.configFieldsVisibleMs, ); const networkIdleTimes = measurements.map( (measurement) => measurement.networkIdleMs, ); const evidence = { metric: "tenant-profile-local-performance", tenantId: targetTenantId, actualApiBaseUrl, measuredAt: new Date().toISOString(), browser: testInfo.project.name, samples: measurements, summary: { configFieldsVisibleMs: { min: Math.min(...configTimes), max: Math.max(...configTimes), p50: percentile(configTimes, 0.5), p95: percentile(configTimes, 0.95), }, networkIdleMs: { min: Math.min(...networkIdleTimes), max: Math.max(...networkIdleTimes), p50: percentile(networkIdleTimes, 0.5), p95: percentile(networkIdleTimes, 0.95), }, }, screenshotPath, }; const evidencePath = path.join( evidenceDir, "tenant-profile-performance-local.json", ); fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`); console.log(JSON.stringify(evidence, null, 2)); const configVisibleBudgetMs = testInfo.project.name === "firefox" ? 1200 : 500; expect(evidence.summary.configFieldsVisibleMs.p95).toBeLessThanOrEqual( configVisibleBudgetMs, ); }); });