Files

86 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
const path = require("path");
const fs = require("fs");
const { execFileSync } = require("child_process");
const { randomUUID } = require("crypto");
const dependencyRoot = process.env.PLAYWRIGHT_REQUIRE_FROM;
if (!dependencyRoot) throw new Error("PLAYWRIGHT_REQUIRE_FROM is required");
const { chromium } = require(path.join(dependencyRoot, "playwright"));
const chromePath = process.env.CHROME_PATH || "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
const outputDir = path.resolve(process.env.UI_SCREENSHOT_DIR || "test/e2e/screenshots/operations");
const marker = `sse_${randomUUID().replaceAll("-", "").slice(0, 16)}`;
fs.mkdirSync(outputDir, { recursive: true });
function psql(sql) {
return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-U", "voice", "-d", "voice_platform", "-v", "ON_ERROR_STOP=1", "-q", "-c", sql], {
cwd: path.resolve(__dirname, ".."), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
});
}
function insertEvent() {
psql(`INSERT INTO web_event(tenant_id,household_id,subject_id,event_type,resource_type,resource_public_id,status)
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000201','10000000-0000-0000-0000-000000000401',
'device.presence.changed','device',gen_random_uuid(),'${marker}')`);
}
(async () => {
const browser = await chromium.launch({ executablePath: chromePath, headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
const failures = [];
const routeCounts = new Map();
let countResponses = false;
page.on("pageerror", (error) => failures.push(`pageerror: ${error.message}`));
page.on("console", (message) => { if (message.type() === "error") failures.push(`console: ${message.text()}`); });
page.on("response", (response) => {
if (!countResponses) return;
const url = new URL(response.url());
if (!url.pathname.startsWith("/api/v1/") || url.pathname === "/api/v1/events") return;
routeCounts.set(url.pathname, (routeCounts.get(url.pathname) || 0) + 1);
if (response.status() === 429) failures.push(`rate limited: ${url.pathname}`);
else if (response.status() >= 400) failures.push(`http ${response.status()}: ${url.pathname}`);
});
await page.goto("http://localhost:3100", { waitUntil: "networkidle" });
const login = page.getByRole("button", { name: "登录研发环境" });
if (await login.isVisible()) {
await login.click();
await page.locator("#username").fill("owner");
await page.locator("#password").fill("LocalOnly123!");
await page.locator("#kc-login").click();
}
await page.waitForURL("http://localhost:3100/**", { timeout: 15000 });
await page.getByRole("heading", { name: "家庭业务工作台" }).waitFor({ state: "visible", timeout: 15000 });
await page.getByText("状态实时更新", { exact: true }).waitFor({ state: "visible", timeout: 15000 });
// Let the login/navigation burst leave the local user token bucket before
// measuring only the refreshes caused by a sustained event stream.
await page.waitForTimeout(6000);
countResponses = true;
for (let index = 0; index < 8; index += 1) {
insertEvent();
await page.waitForTimeout(650);
}
await page.waitForTimeout(2500);
countResponses = false;
const factRefreshes = [...routeCounts.entries()].filter(([route]) => route.endsWith("/facts")).reduce((sum, [, count]) => sum + count, 0);
const totalBusinessResponses = [...routeCounts.values()].reduce((sum, count) => sum + count, 0);
if (factRefreshes < 2) failures.push(`event refresh did not run enough times to verify coalescing: ${factRefreshes}`);
// The first event may refresh immediately; subsequent refreshes are capped
// at one per two seconds. The measured window includes eight Docker/psql
// inserts and therefore permits at most five batches.
if (factRefreshes > 5) failures.push(`event refresh was not coalesced: facts loaded ${factRefreshes} times`);
if (totalBusinessResponses > 90) failures.push(`event stream amplified into ${totalBusinessResponses} business requests`);
const eventStatus = page.locator(".event-stream-status");
const title = await eventStatus.getAttribute("title");
if (!title || title === "等待状态事件") failures.push("the browser did not observe the injected events");
await page.screenshot({ path: path.join(outputDir, "sse-refresh-coalescing.png"), fullPage: true });
if (failures.length) throw new Error(failures.join("\n"));
process.stdout.write(`SSE refresh coalescing passed: 8 events -> ${factRefreshes} refresh batches, ${totalBusinessResponses} business responses.\n`);
} finally {
try { psql(`DELETE FROM web_event WHERE status='${marker}'`); } catch (error) { process.stderr.write(`cleanup failed: ${error.message}\n`); }
await browser.close();
}
})().catch((error) => { process.stderr.write(`${error.stack || error}\n`); process.exitCode = 1; });