1
0
forked from baron/baron-sso
Files
baron-sso/adminfront/tests/tenant-profile-performance-local.spec.ts
2026-06-18 11:02:48 +09:00

257 lines
8.0 KiB
TypeScript

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<string, number>();
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,
);
});
});