2026-05-26 07:08:26 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
|
const os = require("node:os");
|
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
|
const { chromium } = require("playwright");
|
|
|
|
|
|
|
|
|
|
|
|
const OUTPUT_DIR = path.resolve(__dirname, "..", "tmp", "task490-runtime-surfaces-smoke");
|
|
|
|
|
|
const TASK = "task490-runtime-surfaces-smoke";
|
|
|
|
|
|
const TARGET_BLOCK_ID = "task490-runtime-surface-target";
|
|
|
|
|
|
|
|
|
|
|
|
function screenshotPath(name) {
|
|
|
|
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
|
|
|
return path.join(OUTPUT_DIR, `${name}.png`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fileUrl(localPath) {
|
|
|
|
|
|
return `file://${localPath}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function localMdDocumentId(relativePath) {
|
|
|
|
|
|
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function documentUrl(baseUrl, root, relativePath) {
|
|
|
|
|
|
const url = new URL(`${baseUrl}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
|
|
|
|
|
|
url.searchParams.set("sourceKind", "local_folder");
|
|
|
|
|
|
url.searchParams.set("rootUri", fileUrl(root));
|
|
|
|
|
|
url.searchParams.set("treeView", "filetree");
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function writeWorkspaceManifest(root, ownerId) {
|
|
|
|
|
|
const metadataDir = path.join(root, ".mnote");
|
|
|
|
|
|
fs.mkdirSync(metadataDir, { recursive: true });
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(metadataDir, "workspace.json"),
|
|
|
|
|
|
`${JSON.stringify({
|
|
|
|
|
|
workspaceId: `local-ws:${ownerId}:task490`,
|
|
|
|
|
|
ownerId,
|
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
|
capabilities: ["local_files", "markdown_edit", "asset_upload"],
|
|
|
|
|
|
}, null, 2)}\n`,
|
|
|
|
|
|
"utf8",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveScreenshot(page, name) {
|
|
|
|
|
|
const target = screenshotPath(name);
|
|
|
|
|
|
await page.screenshot({ path: target, fullPage: false });
|
|
|
|
|
|
return target;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 21:08:17 +08:00
|
|
|
|
async function assertMaterialIconSurface(locator, label, minCount) {
|
|
|
|
|
|
const iconCount = await locator.locator('.material-symbols-outlined, .slash-item-icon, .block-drag-menu-icon').count();
|
|
|
|
|
|
if (iconCount < minCount) {
|
|
|
|
|
|
throw new Error(`${label} 至少应渲染 ${minCount} 个图标容器,实际: ${iconCount}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const text = ((await locator.textContent()) || '').trim();
|
|
|
|
|
|
const legacyGlyphs = ['⚙', '×', '✕', '⋮', '◣', '↻'];
|
|
|
|
|
|
const leaked = legacyGlyphs.filter((glyph) => text.includes(glyph));
|
|
|
|
|
|
if (leaked.length > 0) {
|
|
|
|
|
|
throw new Error(`${label} 不应泄漏旧 Unicode 图标: ${leaked.join(', ')}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 07:08:26 +08:00
|
|
|
|
async function waitForRuntimeIsland(page, uiTimeoutMs) {
|
|
|
|
|
|
const root = page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first();
|
|
|
|
|
|
await root.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
|
|
|
|
const editor = page
|
|
|
|
|
|
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
|
|
|
|
|
.first();
|
|
|
|
|
|
await editor.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
() => {
|
|
|
|
|
|
const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
|
|
|
|
const editorNode = host?.querySelector('.editor-surface .ProseMirror[contenteditable="true"]');
|
|
|
|
|
|
return host?.getAttribute("data-runtime-editor-status") !== "error"
|
|
|
|
|
|
&& editorNode instanceof HTMLElement
|
|
|
|
|
|
&& editorNode.isContentEditable;
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
{ timeout: uiTimeoutMs },
|
|
|
|
|
|
);
|
|
|
|
|
|
return editor;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function setBlockHandleFixture(page, uiTimeoutMs) {
|
|
|
|
|
|
await page.evaluate((targetBlockId) => {
|
|
|
|
|
|
const editor = document.querySelector(".editor-surface .ProseMirror")?.editor;
|
|
|
|
|
|
if (!editor) {
|
|
|
|
|
|
throw new Error("找不到 Tiptap editor");
|
|
|
|
|
|
}
|
|
|
|
|
|
editor.commands.setContent(
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "doc",
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "paragraph",
|
|
|
|
|
|
attrs: { blockId: targetBlockId },
|
|
|
|
|
|
content: [{ type: "text", text: "Task490 block handle target" }],
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "paragraph",
|
|
|
|
|
|
attrs: { blockId: "task490-second-block" },
|
|
|
|
|
|
content: [{ type: "text", text: "Task490 slash target" }],
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
editor.commands.focus("end");
|
|
|
|
|
|
}, TARGET_BLOCK_ID);
|
|
|
|
|
|
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
(targetBlockId) => document.querySelector(`[data-block-id="${targetBlockId}"]`) instanceof HTMLElement,
|
|
|
|
|
|
TARGET_BLOCK_ID,
|
|
|
|
|
|
{ timeout: uiTimeoutMs },
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function closePageAiIfOpen(page) {
|
|
|
|
|
|
const closeButton = page.locator('[data-page-ai-action="close"]').first();
|
|
|
|
|
|
if (await closeButton.isVisible().catch(() => false)) {
|
2026-06-25 21:08:17 +08:00
|
|
|
|
await closeButton.click({ force: true }).catch(async () => {
|
|
|
|
|
|
await page.evaluate(() => {
|
|
|
|
|
|
const button = document.querySelector('[data-page-ai-action="close"]');
|
|
|
|
|
|
if (button instanceof HTMLElement) button.click();
|
|
|
|
|
|
}).catch(() => undefined);
|
|
|
|
|
|
});
|
2026-05-26 07:08:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
|
|
|
|
delete require.cache[require.resolve("./tree-shell-smoke-helpers")];
|
|
|
|
|
|
const {
|
|
|
|
|
|
BASE_URL,
|
|
|
|
|
|
UI_TIMEOUT_MS,
|
|
|
|
|
|
assert,
|
|
|
|
|
|
ensureAuthenticated,
|
|
|
|
|
|
} = require("./tree-shell-smoke-helpers");
|
|
|
|
|
|
|
|
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
|
|
|
|
const context = await browser.newContext({
|
|
|
|
|
|
viewport: { width: 1440, height: 960 },
|
|
|
|
|
|
locale: "zh-CN",
|
|
|
|
|
|
extraHTTPHeaders: {
|
|
|
|
|
|
"x-mnote-actor-id": "mnote-e2e",
|
|
|
|
|
|
"x-mnote-actor-type": "user",
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const page = await context.newPage();
|
|
|
|
|
|
const screenshots = {};
|
|
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task490-"));
|
|
|
|
|
|
const relativePath = "RuntimeSurfaces.md";
|
2026-06-01 09:29:12 +08:00
|
|
|
|
const capturedPageAi = {
|
|
|
|
|
|
runs: [],
|
|
|
|
|
|
aborts: [],
|
|
|
|
|
|
};
|
2026-05-26 07:08:26 +08:00
|
|
|
|
writeWorkspaceManifest(root, "mnote-e2e");
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(root, relativePath),
|
|
|
|
|
|
["# Runtime Surfaces", "", "Task490 initial paragraph", ""].join("\n"),
|
|
|
|
|
|
"utf8",
|
|
|
|
|
|
);
|
|
|
|
|
|
let caughtError = null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-01 09:29:12 +08:00
|
|
|
|
await page.route("**/api/hermes/client/gateway/health**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
gateway: { ok: true, status: "mocked" },
|
|
|
|
|
|
profile: { name: "reasonix", modelConfigured: true, apiKeyConfigured: true },
|
|
|
|
|
|
suggestions: [],
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/tools**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ ok: true, tools: [] }),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/profiles**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
active: "mnoteai",
|
|
|
|
|
|
profiles: [{ name: "mnoteai", label: "MNote AI", modelConfigured: true, apiKeyConfigured: true }],
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/ai/agent-profiles**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
agentId: "reasonix",
|
|
|
|
|
|
profiles: [],
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/skills**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ ok: true, categories: [], archived: [] }),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-06-05 23:00:53 +08:00
|
|
|
|
await page.route("**/api/hermes/client/capabilities**", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({ ok: true, runtime: "mnote", categories: [], archived: [] }),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-06-01 09:29:12 +08:00
|
|
|
|
await page.route("**/api/hermes/client/sessions", async (route) => {
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
sessionId: "mnote_task490",
|
|
|
|
|
|
title: "task490",
|
|
|
|
|
|
traceId: "trace_task490",
|
|
|
|
|
|
persistence: "local_ai_session_jsonl",
|
|
|
|
|
|
sessionStorage: "local_private",
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/runs", async (route) => {
|
|
|
|
|
|
capturedPageAi.runs.push(route.request().postData() || "");
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
sessionId: "mnote_task490",
|
|
|
|
|
|
runId: "run_task490_stop",
|
|
|
|
|
|
upstream: { run_id: "run_task490_stop", trace_id: "trace_task490_stop" },
|
|
|
|
|
|
traceId: "trace_task490_stop",
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/events/run_task490_stop", async (route) => {
|
2026-06-25 21:08:17 +08:00
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, UI_TIMEOUT_MS * 2));
|
2026-06-01 09:29:12 +08:00
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
|
|
|
|
|
body: `data: ${JSON.stringify({ event: "message.delta", run_id: "run_task490_stop", delta: "Task490 streaming" })}\n\n`,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
await page.route("**/api/hermes/client/runs/run_task490_stop/abort", async (route) => {
|
|
|
|
|
|
capturedPageAi.aborts.push(route.request().postData() || "");
|
|
|
|
|
|
await route.fulfill({
|
|
|
|
|
|
status: 200,
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
status: "aborted",
|
|
|
|
|
|
runtime: { runId: "run_task490_stop", status: "aborted", queueLength: 0 },
|
|
|
|
|
|
events: [
|
|
|
|
|
|
{ event: "abort.started", runId: "run_task490_stop" },
|
|
|
|
|
|
{ event: "abort.completed", runId: "run_task490_stop" },
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-26 07:08:26 +08:00
|
|
|
|
await ensureAuthenticated(page, context.request);
|
|
|
|
|
|
|
|
|
|
|
|
const url = documentUrl(BASE_URL, root, relativePath);
|
|
|
|
|
|
const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
assert(response, "文档页没有返回响应");
|
|
|
|
|
|
assert(response.status() === 200, `文档页状态码异常: ${response.status()}`);
|
|
|
|
|
|
const editor = await waitForRuntimeIsland(page, UI_TIMEOUT_MS);
|
|
|
|
|
|
|
|
|
|
|
|
const moreButton = page.locator('[data-testid="wolai-page-settings-trigger"]').first();
|
|
|
|
|
|
await moreButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await moreButton.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const settingsPopover = page.locator('[data-testid="wolai-page-settings-popover"]').first();
|
|
|
|
|
|
await settingsPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const settingsText = (await settingsPopover.textContent()) || "";
|
|
|
|
|
|
assert(settingsText.includes("页面选项"), `页面设置 popover 未显示页面选项: ${settingsText}`);
|
|
|
|
|
|
screenshots.pageSettings = await saveScreenshot(page, "01-page-settings");
|
|
|
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
|
|
|
|
|
|
|
|
|
const aiButton = page.locator('[data-testid="wolai-floating-ai"]').first();
|
|
|
|
|
|
await aiButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
2026-07-13 09:00:19 +08:00
|
|
|
|
const floatingAiText = ((await aiButton.textContent()) || "").trim();
|
|
|
|
|
|
assert(floatingAiText === "π", `浮动 AI 入口应退役旧 OpenHub 前端并显示 Pi Lab π: ${floatingAiText}`);
|
2026-05-26 07:08:26 +08:00
|
|
|
|
await aiButton.click({ timeout: UI_TIMEOUT_MS });
|
2026-07-13 09:00:19 +08:00
|
|
|
|
const piDrawer = page.locator('[data-page-ai-pi-lab="drawer"]').first();
|
|
|
|
|
|
await piDrawer.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
assert(await page.locator("[data-page-ai-pi-lab-input]").first().isVisible(), "Pi Lab drawer 必须显示输入框");
|
|
|
|
|
|
const retiredFrontend = await page.evaluate(() => ({
|
|
|
|
|
|
legacyDrawerOpen: Boolean(document.querySelector('[data-testid="wolai-page-ai-drawer"]:not([hidden])')),
|
|
|
|
|
|
opencodeHost: Boolean(document.querySelector('[data-page-ai-opencode-host="true"]')),
|
|
|
|
|
|
opencodeIframe: Boolean(document.querySelector('[data-page-ai-opencode-iframe]')),
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert(retiredFrontend.legacyDrawerOpen === false, "浮动 Pi 入口不能打开旧 Page AI/OpenHub drawer");
|
|
|
|
|
|
assert(retiredFrontend.opencodeHost === false, "浮动 Pi 入口不能打开 opencode/OpenHub host 前端");
|
|
|
|
|
|
assert(retiredFrontend.opencodeIframe === false, "浮动 Pi 入口不能保留 opencode/OpenHub iframe");
|
|
|
|
|
|
screenshots.pageAi = await saveScreenshot(page, "02-pi-lab");
|
|
|
|
|
|
await page.locator("[data-page-ai-pi-lab-close]").click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await piDrawer.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS });
|
2026-06-01 09:29:12 +08:00
|
|
|
|
|
|
|
|
|
|
await setBlockHandleFixture(page, UI_TIMEOUT_MS);
|
2026-05-26 07:08:26 +08:00
|
|
|
|
await editor.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await page.keyboard.type("/");
|
|
|
|
|
|
const slashMenu = page.locator('[data-testid="mnote-leptos-tiptap-slash-menu"]').first();
|
|
|
|
|
|
await slashMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const slashText = (await slashMenu.textContent()) || "";
|
|
|
|
|
|
assert(slashText.trim().length > 0, "slash menu 必须显示可选项");
|
2026-06-25 21:08:17 +08:00
|
|
|
|
await assertMaterialIconSurface(slashMenu, "slash menu", 4);
|
2026-05-26 07:08:26 +08:00
|
|
|
|
screenshots.slashMenu = await saveScreenshot(page, "03-slash-menu");
|
|
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
|
|
|
|
|
|
|
|
const target = page.locator(`[data-block-id="${TARGET_BLOCK_ID}"]`).first();
|
|
|
|
|
|
await target.scrollIntoViewIfNeeded({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const targetBox = await target.boundingBox();
|
|
|
|
|
|
assert(targetBox, "block handle 目标块缺少可测量区域");
|
|
|
|
|
|
await page.mouse.move(targetBox.x + 8, targetBox.y + Math.min(12, Math.max(4, targetBox.height / 2)));
|
|
|
|
|
|
const blockHandle = page.locator('[data-testid="block-drag-handle-trigger"]').first();
|
|
|
|
|
|
await blockHandle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await blockHandle.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const blockMenu = page.locator('[data-testid="block-drag-menu"]').first();
|
|
|
|
|
|
await blockMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const blockMenuText = (await blockMenu.textContent()) || "";
|
|
|
|
|
|
assert(blockMenuText.includes("删除") || blockMenuText.includes("Delete"), `block handle menu 缺少基础操作: ${blockMenuText}`);
|
2026-06-25 21:08:17 +08:00
|
|
|
|
await assertMaterialIconSurface(blockMenu, "block handle menu", 8);
|
2026-05-26 07:08:26 +08:00
|
|
|
|
screenshots.blockHandleMenu = await saveScreenshot(page, "04-block-handle-menu");
|
|
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
task: TASK,
|
|
|
|
|
|
baseUrl: BASE_URL,
|
|
|
|
|
|
root,
|
|
|
|
|
|
relativePath,
|
|
|
|
|
|
documentId: localMdDocumentId(relativePath),
|
|
|
|
|
|
screenshotDir: OUTPUT_DIR,
|
|
|
|
|
|
screenshots,
|
|
|
|
|
|
};
|
|
|
|
|
|
fs.writeFileSync(path.join(OUTPUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
|
|
|
|
console.log(JSON.stringify(result, null, 2));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
caughtError = error;
|
|
|
|
|
|
await saveScreenshot(page, "failure").catch(() => undefined);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
|
|
|
|
await page.close().catch(() => undefined);
|
|
|
|
|
|
await context.close().catch(() => undefined);
|
|
|
|
|
|
await browser.close().catch(() => undefined);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (caughtError) {
|
|
|
|
|
|
throw caughtError;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|