feat: finalize operations model workspace

This commit is contained in:
Codex Local
2026-08-31 11:29:56 +08:00
parent 78e0a19acb
commit a8cdec98d1
15 changed files with 406 additions and 63 deletions
+42 -6
View File
@@ -15,13 +15,12 @@ const sections = [
["conversations", "回忆时间轴", "聊天记录"],
["stories", "故事与记忆", "故事与记忆"],
["subjects", "长者资料", "长者资料"],
["messages", "日程与提醒", "日程与提醒"],
["messages", "提醒与待办", "提醒与待办"],
["members", "家庭管理", "家庭管理"],
["devices", "陪伴设备", "陪伴设备"],
["sharing", "内容分享", "内容分享"],
["privacy", "数据与授权", "数据与授权"],
["billing", "服务用量", "服务用量"],
["models", "大模型管理", "大模型管理"],
["profile", "设置与权限", "设置与权限"],
];
@@ -97,6 +96,43 @@ try {
results.push({ section, heading, layout });
}
await page.getByRole("menuitem", { name: /聊天记录$/ }).click();
await page.getByRole("heading", { name: "回忆时间轴", exact: true, level: 1 }).waitFor();
const timelineHeaders = page.locator(".timeline-day-group__header");
await timelineHeaders.first().waitFor();
const timelineHeaderCount = await timelineHeaders.count();
const timelineDefaultsWork = timelineHeaderCount >= 3
&& await timelineHeaders.nth(0).getAttribute("aria-expanded") === "true"
&& await timelineHeaders.nth(1).getAttribute("aria-expanded") === "true"
&& await timelineHeaders.nth(2).getAttribute("aria-expanded") === "false";
const timelineLayout = await page.locator(".memory-timeline-grid .conversation-list").evaluate((element) => ({
height: element.getBoundingClientRect().height,
viewportHeight: window.innerHeight,
overflowY: getComputedStyle(element.querySelector(".memory-timeline-scroll")).overflowY,
}));
const timelineIsBounded = timelineLayout.height <= 721 && timelineLayout.height < timelineLayout.viewportHeight && timelineLayout.overflowY === "auto";
await timelineHeaders.nth(2).click();
const olderTimelineExpands = await timelineHeaders.nth(2).getAttribute("aria-expanded") === "true";
await timelineHeaders.nth(2).click();
const portalSearch = page.getByPlaceholder("搜索长者、设备、通话", { exact: true });
await portalSearch.fill("设备");
await portalSearch.press("Enter");
await page.getByRole("heading", { name: "陪伴设备", exact: true, level: 1 }).waitFor();
const searchNavigationWorks = new URL(page.url()).searchParams.get("section") === "devices";
await page.getByRole("button", { name: "打开提醒与待办", exact: true }).click();
await page.getByRole("heading", { name: "提醒与待办", exact: true, level: 1 }).waitFor();
const reminderNavigationWorks = new URL(page.url()).searchParams.get("section") === "messages";
const reminderPaletteWorks = await page.evaluate(() => {
const controls = [...document.querySelectorAll(".reminder-todo, .reminder-history__summary > button")];
return controls.length > 0 && controls.every((item) => {
const channels = getComputedStyle(item).backgroundColor.match(/\d+/g)?.slice(0, 3).map(Number) ?? [];
return channels.length === 3 && channels.reduce((sum, value) => sum + value, 0) / 3 > 210;
});
});
const customerModelNavigationHidden = await page.getByRole("menuitem", { name: /大模型管理$/ }).count() === 0;
await page.getByRole("menuitem", { name: /长者资料$/ }).click();
await page.getByRole("heading", { name: "长者资料", exact: true, level: 1 }).waitFor();
await page.locator("button.subject-add-button").click();
@@ -107,7 +143,8 @@ try {
await page.screenshot({ path: resolve(outputDirectory, "subjects-avatar-upload.png") });
const disabledCursorResult = await page.evaluate(() => {
const controls = [...document.querySelectorAll("button:disabled")];
return { count: controls.length, allWait: controls.length > 0 && controls.every((item) => getComputedStyle(item).cursor === "wait") };
const cursors = controls.map((item) => ({ className: item.className, cursor: getComputedStyle(item).cursor }));
return { count: controls.length, cursors, allNotAllowed: controls.length > 0 && cursors.every((item) => item.cursor === "not-allowed") };
});
await page.getByRole("button", { name: "关闭新增长者弹窗" }).click();
@@ -132,7 +169,6 @@ try {
await checkModal("陪伴设备", ".workspace--devices .workspace__actions button:has-text('绑定设备')", "绑定设备", "绑定新设备");
await checkModal("内容分享", ".subject-list-toolbar > button", "新建分享", "新建资源分享");
await checkModal("数据与授权", ".privacy-actions button:has-text('资料更正')", "资料更正", "提交资料更正");
await checkModal("大模型管理", ".model-active-hero > button", "添加大模型", "添加总结大模型");
await page.getByRole("menuitem", { name: /故事与记忆$/ }).click();
await page.locator("button.story-candidate-card").first().click();
@@ -146,9 +182,9 @@ try {
const uniqueResponseErrors = [...new Set(errorResponses)].filter((item) => !item.includes("/api/v1/events") && !(/^403 .*\/media$/.test(item)));
const relevantConsoleErrors = consoleErrors.filter((item) => !item.startsWith("Failed to load resource:"));
const report = { results, avatarPreviewVisible, modalChecks, disabledCursorResult, familyNavigationVisible, pageFailures, consoleErrors: relevantConsoleErrors, errorResponses: uniqueResponseErrors, failedRequests };
const report = { results, timelineHeaderCount, timelineDefaultsWork, timelineIsBounded, olderTimelineExpands, searchNavigationWorks, reminderNavigationWorks, reminderPaletteWorks, customerModelNavigationHidden, avatarPreviewVisible, modalChecks, disabledCursorResult, familyNavigationVisible, pageFailures, consoleErrors: relevantConsoleErrors, errorResponses: uniqueResponseErrors, failedRequests };
console.log(JSON.stringify(report, null, 2));
if (!avatarPreviewVisible || modalChecks.some((item) => !item.passed) || !disabledCursorResult.allWait || !familyNavigationVisible || pageFailures.length || relevantConsoleErrors.length || uniqueResponseErrors.length || failedRequests.length) {
if (!timelineDefaultsWork || !timelineIsBounded || !olderTimelineExpands || !searchNavigationWorks || !reminderNavigationWorks || !reminderPaletteWorks || !customerModelNavigationHidden || !avatarPreviewVisible || modalChecks.some((item) => !item.passed) || !disabledCursorResult.allNotAllowed || !familyNavigationVisible || pageFailures.length || relevantConsoleErrors.length || uniqueResponseErrors.length || failedRequests.length) {
throw new Error("Customer portal v0.74 visual/function smoke failed");
}
} finally {
+110
View File
@@ -0,0 +1,110 @@
import { createRequire } from "node:module";
const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url);
const { chromium } = require("playwright");
const webURL = process.env.VISUAL_WEB_URL ?? "http://localhost:3100";
const username = process.env.VISUAL_USERNAME ?? "operator";
const password = process.env.VISUAL_PASSWORD ?? "OpsLocalOnly123!";
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: 1000 }, deviceScaleFactor: 1 });
const consoleErrors = [];
const failedRequests = [];
const errorResponses = [];
page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); });
page.on("pageerror", (error) => consoleErrors.push(`pageerror: ${error.message}`));
page.on("response", (response) => { if (response.status() >= 400) errorResponses.push(`${response.status()} ${response.url()}`); });
page.on("requestfailed", (request) => {
const reason = request.failure()?.errorText ?? "failed";
const pathname = new URL(request.url()).pathname;
if (request.method() === "GET" && pathname === "/api/v1/events" && reason.includes("ERR_ABORTED")) return;
if (pathname.startsWith("/api/v1/") && reason.includes("ERR_ABORTED")) return;
failedRequests.push(`${request.method()} ${request.url()} ${reason}`);
});
const areas = [
["总览", "维护总览", null],
["设备管理", "设备管理", "devices"],
["运行配置", "运行配置", "config"],
["大模型管理", "大模型管理", "models"],
["内容质量", "内容质量", "quality"],
["用量、费用与额度", "用量、费用与额度", "billing"],
["告警处置", "告警处置", "alerts"],
["联调与诊断", "联调与诊断", "diagnostics"],
];
try {
await page.goto(`${webURL}/maintenance`, { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "登录维护中心", exact: true }).click();
await page.waitForURL(/localhost:58180|127\.0\.0\.1:58180/);
await page.locator("#username").fill(username);
await page.locator("#password").fill(password);
await page.locator("#kc-login").click();
await page.waitForURL(new RegExp(`^${new URL(webURL).origin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/maintenance(?:[?#]|$)`), { timeout: 15000 });
await page.getByRole("heading", { name: "维护总览", exact: true, level: 1 }).waitFor({ timeout: 15000 });
const navigation = page.getByRole("navigation", { name: "维护中心主导航" });
const areaResults = [];
for (const [label, heading, area] of areas) {
await navigation.getByRole("button", { name: label, exact: true }).click();
await page.getByRole("heading", { name: heading, exact: true, level: 1 }).waitFor();
areaResults.push({ label, area: new URL(page.url()).searchParams.get("area"), expectedArea: area });
}
const search = page.getByPlaceholder("搜索设备、通话、作业", { exact: true });
await search.fill("告警");
await search.press("Enter");
await page.getByRole("heading", { name: "告警处置", exact: true, level: 1 }).waitFor();
const alertSearchWorks = new URL(page.url()).searchParams.get("area") === "alerts";
await search.fill("10000000-0000-0000-0000-000000000511");
await search.press("Enter");
await page.getByRole("heading", { name: "联调与诊断", exact: true, level: 1 }).waitFor();
const identifierSearchWorks = new URL(page.url()).searchParams.get("area") === "diagnostics";
await navigation.getByRole("button", { name: "大模型管理", exact: true }).click();
await page.getByRole("heading", { name: "大模型管理", exact: true, level: 1 }).waitFor();
await page.getByRole("button", { name: /添加大模型/ }).click();
const modelDialog = page.getByRole("dialog", { name: "添加总结大模型", exact: true });
await modelDialog.waitFor();
const providerButtons = modelDialog.locator(".model-provider-group button");
const modelPaletteWorks = await providerButtons.evaluateAll((controls) => controls.length > 0 && controls.every((item) => {
const channels = getComputedStyle(item).backgroundColor.match(/\d+/g)?.slice(0, 3).map(Number) ?? [];
return channels.length === 3 && channels.reduce((sum, value) => sum + value, 0) / 3 > 210;
}));
await modelDialog.getByRole("button", { name: "关闭", exact: true }).click();
await navigation.getByRole("button", { name: "设备管理", exact: true }).click();
await page.getByRole("button", { name: /新增设备/ }).click();
const createDialog = page.getByRole("dialog", { name: "新增设备", exact: true });
await createDialog.waitFor();
await createDialog.getByRole("button", { name: "关闭", exact: true }).click();
await page.getByRole("button", { name: /编辑/ }).first().click();
const editDialog = page.getByRole("dialog", { name: "编辑设备", exact: true });
await editDialog.waitFor();
await editDialog.getByRole("button", { name: "关闭", exact: true }).click();
await navigation.getByRole("button", { name: "联调与诊断", exact: true }).click();
await page.getByPlaceholder("输入完整编号", { exact: true }).fill("10000000-0000-0000-0000-000000000511");
await page.getByRole("button", { name: "查询", exact: true }).click();
await page.getByRole("button", { name: "导出脱敏诊断包", exact: true }).waitFor();
await page.getByRole("button", { name: "导出脱敏诊断包", exact: true }).click();
const exportForm = page.getByRole("form").last();
const exportSubmit = exportForm.getByRole("button", { name: "生成并下载", exact: true });
const exportInitiallyDisabled = await exportSubmit.isDisabled();
await exportForm.getByRole("textbox", { name: "导出原因", exact: true }).fill("页面功能测试,不下载");
const exportReady = await exportSubmit.isEnabled();
await exportForm.getByRole("button", { name: "取消", exact: true }).click();
const areaFailures = areaResults.filter((item) => item.area !== item.expectedArea);
const relevantResponseErrors = [...new Set(errorResponses)].filter((item) => !item.includes("/api/v1/events") && !(item.startsWith("404 ") && item.endsWith("/api/v1/ops/diagnostics/platform-health")));
const relevantConsoleErrors = consoleErrors.filter((item) => !item.startsWith("Failed to load resource:"));
const report = { areaResults, areaFailures, alertSearchWorks, identifierSearchWorks, modelPaletteWorks, exportInitiallyDisabled, exportReady, consoleErrors: relevantConsoleErrors, errorResponses: relevantResponseErrors, failedRequests };
console.log(JSON.stringify(report, null, 2));
if (areaFailures.length || !alertSearchWorks || !identifierSearchWorks || !modelPaletteWorks || !exportInitiallyDisabled || !exportReady || relevantConsoleErrors.length || relevantResponseErrors.length || failedRequests.length) {
throw new Error("Maintenance navigation/function smoke failed");
}
} finally {
await browser.close();
}