Files
story/test/e2e/visual-ops-config-version-transition.mjs

136 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRequire } from "node:module";
import { randomUUID } from "node:crypto";
import { execFileSync } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require("playwright");
const outputDirectory = new URL("./screenshots/", import.meta.url);
const webURL = process.env.VISUAL_WEB_URL ?? "http://localhost:3100";
const webHostPattern = new RegExp(new URL(webURL).host.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
const oidcURLPattern = new RegExp(process.env.VISUAL_OIDC_URL_PATTERN ?? "localhost:58180");
const username = process.env.VISUAL_USERNAME ?? "owner";
const password = process.env.VISUAL_PASSWORD ?? "LocalOnly123!";
const marker = randomUUID();
const subjectID = "10000000-0000-0000-0000-000000000411";
const fixtures = [
{ internalID: randomUUID(), publicID: randomUUID(), key: `visual-submit-${marker}`.slice(0, 90), name: `页面提交审批夹具-${marker.slice(0, 8)}`, status: "DRAFT", rowVersion: 11 },
{ internalID: randomUUID(), publicID: randomUUID(), key: `visual-publish-${marker}`.slice(0, 90), name: `页面审批发布夹具-${marker.slice(0, 8)}`, status: "PENDING_APPROVAL", rowVersion: 12 },
{ internalID: randomUUID(), publicID: randomUUID(), key: `visual-rollback-${marker}`.slice(0, 90), name: `页面历史回滚夹具-${marker.slice(0, 8)}`, status: "RETIRED", rowVersion: 13 }
];
await mkdir(outputDirectory, { recursive: true });
function psql(statement) {
return execFileSync("docker", ["compose", "exec", "-T", "postgres", "psql", "-v", "ON_ERROR_STOP=1", "-U", "voice", "-d", "voice_platform", "-Atc", statement], {
cwd: process.cwd(), encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]
}).trim();
}
function cleanup() {
psql(`DELETE FROM runtime_config_version WHERE id IN (${fixtures.map((item) => `'${item.internalID}'::uuid`).join(",")});`);
}
psql(`BEGIN;
INSERT INTO runtime_config_version(id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,row_version)
VALUES('${fixtures[0].internalID}'::uuid,'${fixtures[0].publicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${fixtures[0].key}','${fixtures[0].name}','ROUTING',1,'DRAFT','{"default_engine_mode":"s2s_hybrid"}'::jsonb,'{}'::jsonb,'确认草稿提交审批不会直接发布','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,${fixtures[0].rowVersion});
INSERT INTO runtime_config_version(id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,submitted_by,submitted_at,row_version)
VALUES('${fixtures[1].internalID}'::uuid,'${fixtures[1].publicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${fixtures[1].key}','${fixtures[1].name}','ROUTING',1,'PENDING_APPROVAL','{"default_engine_mode":"s2s_hybrid"}'::jsonb,'{}'::jsonb,'确认审批发布范围和业务影响','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,now()-interval '1 hour',${fixtures[1].rowVersion});
INSERT INTO runtime_config_version(id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,secret_refs,change_summary,validation_result,created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at,retired_at,row_version)
VALUES('${fixtures[2].internalID}'::uuid,'${fixtures[2].publicID}'::uuid,'10000000-0000-0000-0000-000000000001'::uuid,'${fixtures[2].key}','${fixtures[2].name}','ROUTING',1,'RETIRED','{"default_engine_mode":"s2s_hybrid"}'::jsonb,'{}'::jsonb,'确认历史内容回滚目标和范围','{}'::jsonb,'10000000-0000-0000-0000-000000000101'::uuid,'10000000-0000-0000-0000-000000000101'::uuid,now()-interval '4 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '3 hours','10000000-0000-0000-0000-000000000102'::uuid,now()-interval '3 hours',now()-interval '2 hours',${fixtures[2].rowVersion});
COMMIT;`);
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? "C:/Program Files/Google/Chrome/Application/chrome.exe" });
const page = await browser.newPage({ viewport: { width: 1440, height: 1050 }, deviceScaleFactor: 1 });
const consoleErrors = [];
const failedRequests = [];
const mutations = [];
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); });
page.on("request", (request) => {
if ((request.method() === "PATCH" && /\/api\/v1\/ops\/config-versions\/[^/]+\/(submit|publish)$/.test(request.url())) || (request.method() === "POST" && /\/api\/v1\/ops\/config-versions\/[^/]+\/rollback$/.test(request.url()))) mutations.push(`${request.method()} ${request.url()}`);
});
page.on("requestfailed", (request) => {
const failure = request.failure()?.errorText ?? "failed";
const requestURL = new URL(request.url());
if (request.method() === "GET" && requestURL.pathname === "/api/v1/events" && failure.includes("ERR_ABORTED")) return;
failedRequests.push(`${request.method()} ${request.url()} ${failure}`);
});
try {
await page.goto(`${webURL}/?workspace=config`, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "登录研发环境" }).click();
await page.waitForURL(oidcURLPattern);
await page.locator("#username").fill(username);
await page.locator("#password").fill(password);
await page.locator("#kc-login").click();
await page.waitForURL(webHostPattern, { timeout: 15000 });
await page.getByRole("heading", { name: "配置发布与版本控制" }).waitFor();
const submitGroup = page.locator(".ops-version-group").filter({ hasText: fixtures[0].name });
await submitGroup.getByRole("button", { name: "提交审批" }).click();
let form = submitGroup.getByRole("form", { name: "配置版本受控流转" });
await form.waitFor();
let text = await form.textContent();
const submitTargetBound = text?.includes(fixtures[0].publicID) === true && text?.includes(`行版本 v${fixtures[0].rowVersion}`) === true && text?.includes("不会发布配置或改变现有投放") === true;
let action = form.getByRole("button", { name: "确认提交审批" });
const submitNeedsAcknowledgement = await action.isDisabled();
await form.getByLabel(/我确认目标版本、变更说明和配置内容已经复核/).check();
const submitEnabled = await action.isEnabled();
await form.getByRole("button", { name: "取消" }).click();
const submitCancelClosed = await form.count() === 0;
const publishGroup = page.locator(".ops-version-group").filter({ hasText: fixtures[1].name });
await publishGroup.getByRole("button", { name: "审批并发布" }).click();
form = publishGroup.getByRole("form", { name: "配置版本受控流转" });
await form.waitFor();
text = await form.textContent();
const publishTargetBound = text?.includes(fixtures[1].publicID) === true && text?.includes(`行版本 v${fixtures[1].rowVersion}`) === true && text?.includes("双人审批") === true;
action = form.getByRole("button", { name: "确认审批并发布" });
const publishEmptyDisabled = await action.isDisabled();
await form.getByLabel("审批发布依据").fill("离线评测通过,审批职责与观察计划已经复核");
await form.getByLabel("灰度比例(%)").fill("25");
await form.getByLabel("并发影响上限").fill("30");
const publishNeedsAcknowledgement = await action.isDisabled();
await form.getByLabel(/我确认离线评测、审批职责和发布范围/).check();
const publishTenantEnabled = await action.isEnabled();
await form.getByLabel("范围类型").selectOption("SUBJECT");
const publishSubjectNeedsID = await action.isDisabled();
await form.getByLabel("试验对象公开编号").fill(subjectID);
const publishSubjectNeedsNewAcknowledgement = await action.isDisabled();
await form.getByLabel(/我确认离线评测、审批职责和发布范围/).check();
text = await form.textContent();
const publishRangeBound = text?.includes(subjectID) === true && text?.includes("100%灰度") === true && text?.includes("并发上限30") === true;
const publishSubjectEnabled = await action.isEnabled();
await form.getByRole("button", { name: "取消" }).click();
const publishCancelClosed = await form.count() === 0;
const rollbackGroup = page.locator(".ops-version-group").filter({ hasText: fixtures[2].name });
await rollbackGroup.getByRole("button", { name: "回滚到此内容" }).click();
form = rollbackGroup.getByRole("form", { name: "配置版本受控流转" });
await form.waitFor();
text = await form.textContent();
const rollbackTargetBound = text?.includes(fixtures[2].publicID) === true && text?.includes(`行版本 v${fixtures[2].rowVersion}`) === true && text?.includes("历史内容回滚") === true;
action = form.getByRole("button", { name: "确认创建回滚版本" });
const rollbackEmptyDisabled = await action.isDisabled();
await form.getByLabel("回滚依据和故障说明").fill("新版本出现语音路由异常,选择已验证历史内容恢复服务");
const rollbackNeedsAcknowledgement = await action.isDisabled();
await form.getByLabel(/我确认已核对目标历史内容和回滚范围/).check();
const rollbackEnabled = await action.isEnabled();
await form.screenshot({ path: fileURLToPath(new URL("ops-config-version-transition.png", outputDirectory)) });
await form.getByRole("button", { name: "取消" }).click();
const rollbackCancelClosed = await form.count() === 0;
const overflow = await page.evaluate(() => [...document.querySelectorAll("body *")]
.filter((element) => element instanceof HTMLElement && element.scrollWidth > element.clientWidth + 2 && getComputedStyle(element).overflowX === "visible")
.slice(0, 20)
.map((element) => `${element.tagName}.${element.className}: ${element.clientWidth}/${element.scrollWidth}`));
const states = psql(`SELECT public_id||'|'||status||'|'||row_version FROM runtime_config_version WHERE id IN (${fixtures.map((item) => `'${item.internalID}'::uuid`).join(",")}) ORDER BY status;`).split(/\r?\n/).filter(Boolean);
const unchanged = fixtures.every((item) => states.includes(`${item.publicID}|${item.status}|${item.rowVersion}`));
const result = { submitTargetBound, submitNeedsAcknowledgement, submitEnabled, submitCancelClosed, publishTargetBound, publishEmptyDisabled, publishNeedsAcknowledgement, publishTenantEnabled, publishSubjectNeedsID, publishSubjectNeedsNewAcknowledgement, publishRangeBound, publishSubjectEnabled, publishCancelClosed, rollbackTargetBound, rollbackEmptyDisabled, rollbackNeedsAcknowledgement, rollbackEnabled, rollbackCancelClosed, unchanged, mutations, overflow, consoleErrors, failedRequests };
console.log(JSON.stringify(result, null, 2));
if (Object.entries(result).some(([key, value]) => !["mutations", "overflow", "consoleErrors", "failedRequests"].includes(key) && value !== true) || mutations.length || overflow.length || consoleErrors.length || failedRequests.length) throw new Error("Ops config version transition visual smoke failed");
} finally {
await browser.close();
cleanup();
}