forked from baron/baron-sso
257 lines
8.0 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|