Files
mnote/scripts/task763-page-ai-opencode-embed-smoke.js
T
2026-06-25 21:08:17 +08:00

299 lines
13 KiB
JavaScript

#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("playwright");
const {
BASE_URL,
UI_TIMEOUT_MS,
ensureAuthenticated,
} = require("./tree-shell-smoke-helpers");
const TASK = "task763-page-ai-opencode-embed-smoke";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", TASK);
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
async function saveScreenshot(page, name) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const target = path.join(OUTPUT_DIR, `${name}.png`);
await page.screenshot({ path: target, fullPage: true });
return target;
}
async function waitForVisibleAny(page, selectors, label) {
await page.waitForFunction(
(candidateSelectors) => candidateSelectors.some((selector) => {
const nodes = Array.from(document.querySelectorAll(selector));
return nodes.some((node) => {
if (!(node instanceof HTMLElement)) return false;
const style = window.getComputedStyle(node);
const rect = node.getBoundingClientRect();
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
});
}),
selectors,
{ timeout: UI_TIMEOUT_MS },
).catch((error) => {
throw new Error(`${label} 不可见。候选选择器: ${selectors.join(", ")}\n${error.message}`);
});
}
async function collectOpencodeHooks(page) {
return page.evaluate(() => {
const visible = (node) => {
if (!(node instanceof HTMLElement)) return false;
const style = window.getComputedStyle(node);
const rect = node.getBoundingClientRect();
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
};
const bySelector = (selectors) => selectors.flatMap((selector) =>
Array.from(document.querySelectorAll(selector)).map((node) => ({
selector,
tag: node.tagName.toLowerCase(),
text: (node.textContent || "").trim().slice(0, 120),
visible: visible(node),
href: node.getAttribute("href") || "",
src: node.getAttribute("src") || "",
action: node.getAttribute("data-page-ai-action") || "",
testid: node.getAttribute("data-testid") || "",
})),
);
const hostSelectors = [
"[data-page-ai-opencode-host]",
"[data-testid='page-ai-opencode-host']",
"[data-page-ai-host-chrome]",
"[data-page-ai-opencode-chrome]",
".wolai-page-ai-opencode-host",
".wolai-page-ai-opencode-chrome",
".wolai-page-ai-host-chrome",
];
const frameSelectors = [
"iframe[data-page-ai-opencode-iframe]",
"iframe[data-page-ai-opencode-frame]",
"iframe[data-testid='page-ai-opencode-frame']",
"iframe[src*='/page-ai/opencode']",
"iframe[src*='opencode']",
];
const changedFileSelectors = [
"[data-page-ai-opencode-changed-files]",
"[data-page-ai-changed-file-chip]",
"[data-page-ai-opencode-open-file]",
"[data-page-ai-changed-file]",
"[data-page-ai-action='open-changed-file']",
"[data-page-ai-action='open-file']",
"[data-page-ai-board-run-detail-card] .wolai-page-ai-tool-details",
".wolai-page-ai-changed-file-chip",
".wolai-page-ai-changed-files",
];
const openSelectors = [
"[data-page-ai-opencode-open-file]",
"[data-page-ai-action='open-changed-file']",
"[data-page-ai-action='open-current-file']",
"[data-page-ai-action='open-current-page']",
"[data-page-ai-action='open-file']",
"[data-page-ai-open-file]",
];
const refreshSelectors = [
"[data-page-ai-action='opencode-refresh-current-page']",
"[data-page-ai-refresh-file]",
"[data-page-ai-action='refresh-current-file']",
"[data-page-ai-action='refresh-current-page']",
"[data-page-ai-action='refresh-file']",
"[data-page-ai-refresh-file]",
];
return {
title: document.title,
url: location.href,
drawerVisible: Boolean(Array.from(document.querySelectorAll("[data-testid='wolai-page-ai-drawer']")).find(visible)),
hostChrome: bySelector(hostSelectors),
frames: bySelector(frameSelectors),
changedFiles: bySelector(changedFileSelectors),
openHooks: bySelector(openSelectors),
refreshHooks: bySelector(refreshSelectors),
htmlFlags: {
receiptCurrentRefresh: document.documentElement.getAttribute("data-mnote-page-ai-receipt-current-refresh") || "",
receiptFiletreeRefresh: document.documentElement.getAttribute("data-mnote-page-ai-receipt-filetree-refresh") || "",
},
};
});
}
function assertAnyVisible(items, label) {
assert(
items.some((item) => item.visible),
`${label} 缺失或不可见: ${JSON.stringify(items, null, 2)}`,
);
}
function assertAnyHook(items, label) {
assert(items.length > 0, `${label} DOM hook 缺失`);
}
async function main() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const screenshots = [];
const consoleMessages = [];
let result = null;
const browser = await chromium.launch({
headless: process.env.HEADFUL !== "1",
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
});
const context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const page = await context.newPage();
page.on("console", (message) => {
if (["error", "warning"].includes(message.type())) {
consoleMessages.push({ type: message.type(), text: message.text() });
}
});
try {
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const quickLogin = page.getByRole("button", { name: "测试账号快速登录" });
if (await quickLogin.count()) {
await quickLogin.click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined);
} else {
await ensureAuthenticated(page, context.request);
}
await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.waitForSelector('[data-testid="wolai-floating-ai"]', { timeout: UI_TIMEOUT_MS });
const firstVisibleMarkdown = page.locator('[data-document-id^="local-md:"] button.tree-link, button[data-document-id^="local-md:"]').filter({ visible: true });
if (await firstVisibleMarkdown.count()) {
await firstVisibleMarkdown.first().click({ timeout: UI_TIMEOUT_MS });
await page.waitForTimeout(1200);
}
await waitForVisibleAny(page, ["[data-testid='wolai-floating-ai']", "[data-testid='wolai-page-ai-drawer']"], "Page AI 入口");
const drawerAlreadyOpen = await page.locator("[data-testid='wolai-page-ai-drawer']").count().then(async (count) => {
if (!count) return false;
return page.locator("[data-testid='wolai-page-ai-drawer']").first().isVisible().catch(() => false);
});
if (!drawerAlreadyOpen) {
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
}
await waitForVisibleAny(page, ["[data-testid='wolai-page-ai-drawer']"], "Page AI 抽屉");
await waitForVisibleAny(
page,
[
"[data-page-ai-opencode-host]",
"[data-testid='page-ai-opencode-host']",
"[data-page-ai-host-chrome]",
"[data-page-ai-opencode-chrome]",
".wolai-page-ai-opencode-host",
".wolai-page-ai-opencode-chrome",
".wolai-page-ai-host-chrome",
"iframe[data-page-ai-opencode-iframe]",
"iframe[data-page-ai-opencode-frame]",
"iframe[data-testid='page-ai-opencode-frame']",
"iframe[src*='/page-ai/opencode']",
"iframe[src*='opencode']",
],
"opencode iframe 或 host chrome",
);
await page.waitForFunction(
() => {
const frame = document.querySelector("iframe[data-page-ai-opencode-iframe]");
return frame instanceof HTMLIFrameElement && /\/session\/ses_/.test(frame.src || "");
},
{ timeout: UI_TIMEOUT_MS },
);
const hooks = await collectOpencodeHooks(page);
assert(hooks.drawerVisible, "Page AI 抽屉未保持可见");
assertAnyVisible([...hooks.hostChrome, ...hooks.frames], "opencode iframe/host chrome");
assert(
hooks.frames.some((frame) => /\/session\/ses_/.test(frame.src || "")),
`opencode iframe 未进入绑定 session URL: ${JSON.stringify(hooks.frames, null, 2)}`,
);
assert(!hooks.hostChrome.some((item) => /Agent Board|ZCode|Hermes|Reasonix/.test(item.text || "")), "opencode host chrome 混入旧 Page AI provider 文案");
assertAnyHook(hooks.changedFiles, "changed files 容器或 hook");
assertAnyHook(hooks.refreshHooks, "refresh file hook");
screenshots.push(await saveScreenshot(page, "opencode-page-ai-open"));
const firstSessionUrl = hooks.frames.find((frame) => /\/session\/ses_/.test(frame.src || ""))?.src || "";
const secondContext = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
const secondPage = await secondContext.newPage();
secondPage.on("console", (message) => {
if (["error", "warning"].includes(message.type())) {
consoleMessages.push({ type: `second:${message.type()}`, text: message.text() });
}
});
try {
await secondPage.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const secondQuickLogin = secondPage.getByRole("button", { name: "测试账号快速登录" });
if (await secondQuickLogin.count()) {
await secondQuickLogin.click({ timeout: UI_TIMEOUT_MS });
await secondPage.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined);
} else {
await ensureAuthenticated(secondPage, secondContext.request);
}
await secondPage.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await secondPage.waitForSelector('[data-testid="wolai-floating-ai"]', { timeout: UI_TIMEOUT_MS });
const secondFirstVisibleMarkdown = secondPage.locator('[data-document-id^="local-md:"] button.tree-link, button[data-document-id^="local-md:"]').filter({ visible: true });
if (await secondFirstVisibleMarkdown.count()) {
await secondFirstVisibleMarkdown.first().click({ timeout: UI_TIMEOUT_MS });
await secondPage.waitForTimeout(1200);
}
await waitForVisibleAny(secondPage, ["[data-testid='wolai-floating-ai']", "[data-testid='wolai-page-ai-drawer']"], "第二浏览器 Page AI 入口");
const secondDrawerAlreadyOpen = await secondPage.locator("[data-testid='wolai-page-ai-drawer']").count().then(async (count) => {
if (!count) return false;
return secondPage.locator("[data-testid='wolai-page-ai-drawer']").first().isVisible().catch(() => false);
});
if (!secondDrawerAlreadyOpen) {
await secondPage.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
}
await secondPage.waitForFunction(
() => {
const frame = document.querySelector("iframe[data-page-ai-opencode-iframe]");
return frame instanceof HTMLIFrameElement && /\/session\/ses_/.test(frame.src || "");
},
{ timeout: UI_TIMEOUT_MS },
);
const secondHooks = await collectOpencodeHooks(secondPage);
const secondSessionUrl = secondHooks.frames.find((frame) => /\/session\/ses_/.test(frame.src || ""))?.src || "";
assert.strictEqual(secondSessionUrl, firstSessionUrl, `跨浏览器 session binding 未复用: first=${firstSessionUrl} second=${secondSessionUrl}`);
screenshots.push(await saveScreenshot(secondPage, "opencode-page-ai-second-browser"));
hooks.secondBrowser = { frames: secondHooks.frames, sessionUrl: secondSessionUrl };
} finally {
await secondContext.close().catch(() => undefined);
}
result = {
ok: true,
baseUrl: BASE_URL,
hooks,
screenshots,
consoleMessages,
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.log(JSON.stringify(result, null, 2));
} catch (error) {
screenshots.push(await saveScreenshot(page, "failure").catch(() => ""));
result = {
ok: false,
baseUrl: BASE_URL,
error: error instanceof Error ? error.stack || error.message : String(error),
screenshots: screenshots.filter(Boolean),
consoleMessages,
hooks: await collectOpencodeHooks(page).catch(() => null),
};
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
throw error;
} finally {
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.stack || error.message : String(error));
process.exit(1);
});