|
|
|
@@ -12,6 +12,7 @@ interface SmokePage {
|
|
|
|
|
path: string;
|
|
|
|
|
readySelector: string;
|
|
|
|
|
screenshotName: string;
|
|
|
|
|
interaction: "none" | "api-key-bindings" | "history-detail";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SmokeProfile {
|
|
|
|
@@ -142,6 +143,7 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
|
|
|
|
|
const network = [];
|
|
|
|
|
const consoleErrors = [];
|
|
|
|
|
const pages = [];
|
|
|
|
|
const interactions = [];
|
|
|
|
|
const screenshots = [];
|
|
|
|
|
let networkCount = 0;
|
|
|
|
|
let consoleCount = 0;
|
|
|
|
@@ -202,6 +204,85 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
|
|
|
|
|
}
|
|
|
|
|
const shot = await screenshot(item.screenshotName);
|
|
|
|
|
screenshots.push(shot);
|
|
|
|
|
if (item.interaction === "api-key-bindings" && dom.rowCount > 0) {
|
|
|
|
|
await page.locator(item.readySelector + " tbody tr").first().click();
|
|
|
|
|
const dialog = page.getByRole("dialog", { name: /Skill 绑定/u });
|
|
|
|
|
await dialog.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
|
|
|
|
|
const select = dialog.getByRole("combobox", { name: "选择要绑定的 Skill" });
|
|
|
|
|
const bindingEvidence = await select.evaluate((element) => {
|
|
|
|
|
const option = element.querySelector("option");
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
return {
|
|
|
|
|
placeholder: option?.textContent?.trim() || null,
|
|
|
|
|
visible: rect.width > 0 && rect.height > 0,
|
|
|
|
|
width: Math.round(rect.width),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
const ok = bindingEvidence.placeholder === "请选择要绑定的 Skill"
|
|
|
|
|
&& bindingEvidence.visible
|
|
|
|
|
&& bindingEvidence.width >= 240;
|
|
|
|
|
if (!ok) addFailure("API Key Skill 下拉框占位符或布局不完整");
|
|
|
|
|
const interactionShot = await screenshot("superapi-api-key-bindings.png");
|
|
|
|
|
screenshots.push(interactionShot);
|
|
|
|
|
interactions.push({ id: item.interaction, ok, ...bindingEvidence, screenshot: interactionShot });
|
|
|
|
|
await dialog.getByRole("button", { name: "关闭" }).click();
|
|
|
|
|
}
|
|
|
|
|
if (item.interaction === "history-detail" && dom.rowCount > 0) {
|
|
|
|
|
await page.locator(item.readySelector + " tbody tr").first().click();
|
|
|
|
|
const dialog = page.getByRole("dialog", { name: "请求详情" });
|
|
|
|
|
await dialog.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
|
|
|
|
|
const readablePanels = dialog.locator(".readable-columns > section");
|
|
|
|
|
const requestPanel = readablePanels.nth(0);
|
|
|
|
|
const requestSections = await readablePanels.nth(0).locator(".readable-section").evaluateAll((elements) =>
|
|
|
|
|
elements.map((element) => element.getAttribute("data-section-id")));
|
|
|
|
|
const responseSections = await readablePanels.nth(1).locator(".readable-section").evaluateAll((elements) =>
|
|
|
|
|
elements.map((element) => element.getAttribute("data-section-id")));
|
|
|
|
|
const readableVisible = await dialog.locator(".readable-columns").isVisible();
|
|
|
|
|
const skillNode = requestPanel.locator('[data-section-id="skills"] .readable-node').first();
|
|
|
|
|
const toolNode = requestPanel.locator('[data-section-id="tools"] .readable-node').first();
|
|
|
|
|
await skillNode.locator("summary").first().click();
|
|
|
|
|
await toolNode.locator("summary").first().click();
|
|
|
|
|
const skillDetailFields = await skillNode.locator(".readable-value").count();
|
|
|
|
|
const toolDetailFields = await toolNode.locator(".readable-value").count();
|
|
|
|
|
const semanticDetailsVisible = skillDetailFields >= 2 && toolDetailFields >= 2;
|
|
|
|
|
await dialog.getByRole("button", { name: "Raw", exact: true }).click();
|
|
|
|
|
const rawVisible = await dialog.locator(".raw-columns").isVisible();
|
|
|
|
|
const rawPanelCount = await dialog.locator(".raw-columns > section > pre").count();
|
|
|
|
|
const readableHiddenInRaw = await dialog.locator(".readable-columns").count() === 0;
|
|
|
|
|
await dialog.getByRole("button", { name: "分块", exact: true }).click();
|
|
|
|
|
const readableRestored = await dialog.locator(".readable-columns").isVisible();
|
|
|
|
|
const requestSemantic = ["systemPrompt", "userPrompts", "skills", "tools"].every((id) => requestSections.includes(id));
|
|
|
|
|
const responseSemantic = ["overview", "outputs", "usage"].every((id) => responseSections.includes(id));
|
|
|
|
|
const ok = readableVisible
|
|
|
|
|
&& requestSemantic
|
|
|
|
|
&& responseSemantic
|
|
|
|
|
&& semanticDetailsVisible
|
|
|
|
|
&& rawVisible
|
|
|
|
|
&& rawPanelCount === 2
|
|
|
|
|
&& readableHiddenInRaw
|
|
|
|
|
&& readableRestored;
|
|
|
|
|
if (!ok) addFailure("请求详情分块或 Raw 模式不可用");
|
|
|
|
|
const interactionShot = await screenshot("superapi-history-detail-readable.png");
|
|
|
|
|
screenshots.push(interactionShot);
|
|
|
|
|
interactions.push({
|
|
|
|
|
id: item.interaction,
|
|
|
|
|
ok,
|
|
|
|
|
requestSections,
|
|
|
|
|
responseSections,
|
|
|
|
|
requestSemantic,
|
|
|
|
|
responseSemantic,
|
|
|
|
|
skillDetailFields,
|
|
|
|
|
toolDetailFields,
|
|
|
|
|
semanticDetailsVisible,
|
|
|
|
|
readableVisible,
|
|
|
|
|
rawVisible,
|
|
|
|
|
rawPanelCount,
|
|
|
|
|
readableHiddenInRaw,
|
|
|
|
|
readableRestored,
|
|
|
|
|
screenshot: interactionShot,
|
|
|
|
|
});
|
|
|
|
|
await dialog.getByRole("button", { name: "关闭" }).click();
|
|
|
|
|
}
|
|
|
|
|
pages.push({ id: item.id, path: new URL(page.url()).pathname, dom, screenshot: shot });
|
|
|
|
|
recordStep("读取 SuperAPI " + item.id + " 页面", { ok: dom.pageId === item.id && !dom.error, path: new URL(page.url()).pathname, rowCount: dom.rowCount });
|
|
|
|
|
}
|
|
|
|
@@ -211,6 +292,7 @@ export default async function superApiManagementSmoke({ page, goto, wait, screen
|
|
|
|
|
profileName: config.profileName,
|
|
|
|
|
configRef: config.configRef,
|
|
|
|
|
pages,
|
|
|
|
|
interactions,
|
|
|
|
|
network: { count: networkCount, items: network, mutatingRequests },
|
|
|
|
|
console: { count: consoleCount, items: consoleErrors },
|
|
|
|
|
failures,
|
|
|
|
@@ -287,6 +369,7 @@ function smokeProfile(value: unknown, name: string): SmokeProfile {
|
|
|
|
|
path: pathValue(item.path, "page.path"),
|
|
|
|
|
readySelector: text(item.readySelector, "page.readySelector"),
|
|
|
|
|
screenshotName: text(item.screenshotName, "page.screenshotName"),
|
|
|
|
|
interaction: interaction(item.interaction),
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
viewport: { width: integer(viewport.width, "viewport.width"), height: integer(viewport.height, "viewport.height") },
|
|
|
|
@@ -301,6 +384,12 @@ function smokeProfile(value: unknown, name: string): SmokeProfile {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function interaction(value: unknown): SmokePage["interaction"] {
|
|
|
|
|
if (value === undefined) return "none";
|
|
|
|
|
if (value === "api-key-bindings" || value === "history-detail") return value;
|
|
|
|
|
throw new Error("page.interaction 必须是 api-key-bindings 或 history-detail");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function record(value: unknown, name: string): Record<string, unknown> {
|
|
|
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${name} 必须是对象`);
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|