Files
mnote/scripts/task506-local-markdown-attachment-ref-matrix-smoke.js
T

519 lines
24 KiB
JavaScript
Raw 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.
#!/usr/bin/env node
"use strict";
const { loginViaAuthForm } = require('./lib/browser-auth-login');
const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { chromium } = require("playwright");
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 45_000);
const PDF_PATH = process.env.MNOTE_TASK506_PDF_PATH || "/home/lix/Downloads/ao2c06124.pdf";
const DOCX_PATH = process.env.MNOTE_TASK506_DOCX_PATH || "/home/lix/Downloads/重庆发展特殊化妆品可行性报告_政府汇报版.docx";
const PNG_PATH = process.env.MNOTE_TASK506_PNG_PATH || "/home/lix/Downloads/image.png";
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task506-local-markdown-attachment-ref-matrix-smoke");
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
const TEST_PASSWORD = process.env.MNOTE_E2E_PASSWORD || ["Mnote", "E2E", "123!"].join("");
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium", "/usr/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
function fileUrl(localPath) {
return `file://${localPath.split(path.sep).map((part, index) => (
index === 0 ? "" : encodeURIComponent(part)
)).join("/")}`;
}
function localMdDocumentId(relativePath) {
return `local-md:${Buffer.from(relativePath, "utf8")
.toString("hex")
.replace(/../g, (hex) => {
const code = Number.parseInt(hex, 16);
const ch = String.fromCharCode(code);
return /[A-Za-z0-9._-]/.test(ch) ? ch : `~${hex.toUpperCase()}`;
})}`;
}
function workspaceId(ownerId) {
return `local-ws:${ownerId}:task506`;
}
function documentUrl(root, relativePath) {
const url = new URL(`${BASE_URL}/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: workspaceId(ownerId),
ownerId,
createdAt: new Date().toISOString(),
capabilities: ["local_files", "markdown_edit", "asset_upload"],
}, null, 2)}\n`,
"utf8",
);
}
async function quickLogin(page) {
// 7-76 P0: 标准表单登录(无测试快速登录按钮)
const base =
(typeof BASE_URL !== "undefined" && BASE_URL) ||
(typeof baseUrl !== "undefined" && baseUrl) ||
process.env.MNOTE_UI_BASE_URL ||
"http://127.0.0.1:3000";
const timeout =
(typeof UI_TIMEOUT_MS !== "undefined" && UI_TIMEOUT_MS) ||
(typeof TIMEOUT !== "undefined" && TIMEOUT) ||
30_000;
if (!String(page.url() || "").includes("/auth")) {
await page.goto(String(base).replace(/\/+$/, "") + "/auth", {
waitUntil: "commit",
timeout,
});
}
await loginViaAuthForm(page, {
baseUrl: base,
timeoutMs: timeout,
gotoAuth: false,
});
await page
.waitForURL((url) => !String(url).includes("/auth"), { timeout })
.catch(() => {});
}
async function ensureTestUser(context) {
const signIn = await context.request.fetch(`${BASE_URL}/api/auth`, {
method: "POST",
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
email: "mnote.e2e@example.com",
password: TEST_PASSWORD,
flow: "signIn",
},
},
},
});
if (signIn.ok()) return;
const signUp = await context.request.fetch(`${BASE_URL}/api/auth`, {
method: "POST",
data: {
action: "auth:signIn",
args: {
provider: "password",
params: {
email: "mnote.e2e@example.com",
username: "mnote-e2e",
name: "mnote-e2e",
password: TEST_PASSWORD,
flow: "signUp",
},
},
},
});
assert(signUp.ok(), `测试用户创建失败: ${signUp.status()} ${await signUp.text()}`);
}
async function openDocument(page, root, relativePath) {
await page.goto(documentUrl(root, relativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
}
async function uploadAttachmentViaSlash(page, filePath) {
await activatePrimaryPageTab(page).catch(() => undefined);
const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first();
await editor.click({ timeout: UI_TIMEOUT_MS });
await page.keyboard.press("End").catch(() => undefined);
await page.keyboard.type("/");
const item = page.locator('.document-pane[data-pane-role="primary"] [data-testid="slash-item-upload-attachment"]').first();
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const [fileChooser] = await Promise.all([
page.waitForEvent("filechooser", { timeout: UI_TIMEOUT_MS }),
item.click({ timeout: UI_TIMEOUT_MS }),
]);
await fileChooser.setFiles(filePath);
}
async function activatePrimaryPageTab(page) {
await page.waitForFunction(
() => typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === "function",
null,
{ timeout: UI_TIMEOUT_MS },
).catch(() => undefined);
await page.evaluate(() => {
if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === "function") {
window.__mnoteDocumentPaneRuntime.activatePageTab({ paneRole: "primary" });
}
}).catch(() => undefined);
const pageTab = page.locator('[data-mnote-main-tab="page"][data-pane-role="primary"]').first();
if (await pageTab.isVisible({ timeout: 1000 }).catch(() => false)) {
await pageTab.click({ timeout: UI_TIMEOUT_MS });
}
await page.waitForFunction(() => !new URL(location.href).searchParams.get("resourceTab"), null, { timeout: UI_TIMEOUT_MS / 2 });
await page.waitForFunction(() => {
const pagePanel = document.querySelector('[data-mnote-page-tab-panel][data-pane-role="primary"]');
const resourceHost = document.querySelector('[data-mnote-resource-tab-host][data-pane-role="primary"]');
const pageTab = document.querySelector('[data-mnote-main-tab="page"][data-pane-role="primary"]');
return pagePanel instanceof HTMLElement
&& pagePanel.hidden === false
&& (!(resourceHost instanceof HTMLElement) || resourceHost.hidden === true)
&& (!(pageTab instanceof HTMLElement) || pageTab.getAttribute("aria-selected") === "true");
}, null, { timeout: UI_TIMEOUT_MS / 2 });
await page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
}
async function waitForNoUploadSaveError(page, label) {
await page.waitForTimeout(1200);
const state = await page.evaluate(() => ({
lastSaveError: document.documentElement.getAttribute("data-mnote-last-upload-save-error") || "",
conflictVisible: Boolean(document.querySelector('[data-testid="mnote-editor-conflict-panel"]')),
inserted: document.documentElement.getAttribute("data-mnote-last-upload-inserted") || "",
}));
assert.equal(state.lastSaveError, "", `${label}: 上传保存不应失败 ${JSON.stringify(state)}`);
assert.equal(state.conflictVisible, false, `${label}: 上传不应触发冲突 ${JSON.stringify(state)}`);
return state;
}
async function readAggregate(page) {
return await page.evaluate(() => {
const script = document.getElementById("__MNOTE_PAGE_AGGREGATE__");
if (!script) return null;
return JSON.parse(script.textContent || "null");
});
}
async function bypassRuntimeAssetCache(context) {
await context.route("**/api/mnote-browser-runtime/**", async (route) => {
const headers = {
...route.request().headers(),
"cache-control": "no-cache",
pragma: "no-cache",
};
await route.continue({ headers });
});
}
function readMarkdown(root, relativePath) {
return fs.readFileSync(path.join(root, relativePath), "utf8");
}
function assertNoRuntimeHref(markdown, label) {
assert(!markdown.includes("/office-preview"), `${label}: Markdown 不应包含 /office-preview\n${markdown}`);
assert(!markdown.includes("/api/local-folder/files/open"), `${label}: Markdown 不应包含本地 open API\n${markdown}`);
}
async function editorAttachmentLinks(page) {
return await page.evaluate(() => Array.from(document.querySelectorAll('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a'))
.map((link) => ({
text: link.textContent || "",
href: link.getAttribute("href") || "",
className: link.getAttribute("class") || "",
missing: link.getAttribute("data-mnote-attachment-missing") || "",
unauthorized: link.getAttribute("data-mnote-attachment-unauthorized") || "",
ariaLabel: link.getAttribute("aria-label") || "",
})));
}
async function clickAttachmentLink(page, text) {
await activatePrimaryPageTab(page);
const link = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a').filter({ hasText: text }).first();
await link.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
for (let attempt = 0; attempt < 4; attempt += 1) {
const visible = await link.evaluate((node) => {
if (!node) return false;
const scrollParent = (() => {
let current = node.parentElement;
while (current && current !== document.body && current !== document.documentElement) {
if (current.scrollHeight > current.clientHeight + 8) return current;
current = current.parentElement;
}
return document.scrollingElement || document.documentElement;
})();
if (typeof node.scrollIntoView === "function") {
node.scrollIntoView({ block: "center", inline: "nearest" });
}
const box = node.getBoundingClientRect();
const targetY = window.innerHeight / 2;
const deltaY = box.y + box.height / 2 - targetY;
if (Math.abs(deltaY) > 8 && scrollParent) {
scrollParent.scrollTop += deltaY;
}
const next = node.getBoundingClientRect();
return next.y >= 0 && next.y + next.height <= window.innerHeight;
});
if (visible) break;
await page.waitForTimeout(120);
}
await link.evaluate((node) => {
if (node && typeof node.scrollIntoView === "function") {
node.scrollIntoView({ block: "center", inline: "nearest" });
}
});
await page.waitForTimeout(120);
const rect = await page.evaluate((needle) => {
const candidates = Array.from(document.querySelectorAll('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a'));
const node = candidates.find((item) => (item.textContent || "").includes(needle));
if (!node) return null;
const box = node.getBoundingClientRect();
const top = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
return {
x: box.x,
y: box.y,
width: box.width,
height: box.height,
viewportHeight: window.innerHeight,
className: node.getAttribute("class") || "",
href: node.getAttribute("href") || "",
topTag: top ? top.tagName : "",
topText: top ? (top.textContent || "").slice(0, 80) : "",
topClass: top instanceof HTMLElement ? (top.getAttribute("class") || "") : "",
};
}, text);
assert(rect, `找不到可点击附件链接: ${text}`);
assert(
rect.y >= 0 && rect.y + rect.height <= rect.viewportHeight,
`附件链接未滚动到可点击视口内: ${text} ${JSON.stringify(rect)}`,
);
await page.mouse.click(rect.x + rect.width / 2, rect.y + rect.height / 2);
await page.waitForTimeout(180);
return await page.evaluate((needle) => ({
needle,
blocked: document.documentElement.getAttribute("data-mnote-attachment-open-blocked") || "",
pageHidden: document.querySelector('[data-mnote-page-tab-panel][data-pane-role="primary"]')?.hidden ?? null,
resourceHidden: document.querySelector('[data-mnote-resource-tab-host][data-pane-role="primary"]')?.hidden ?? null,
activePageTab: document.querySelector('[data-mnote-main-tab="page"][data-pane-role="primary"]')?.getAttribute("aria-selected") || "",
}), text);
}
async function waitForActiveResource(page, text, label) {
await page.waitForFunction(
(needle) => {
const tab = document.querySelector('[data-testid="document-resource-tab"][data-active="true"], [data-mnote-resource-tab][data-active="true"]');
const panel = document.querySelector('.mnote-resource-tab-panel:not([hidden])');
const frame = panel?.querySelector?.('iframe.mnote-resource-tab-frame');
const frameSrc = frame instanceof HTMLIFrameElement ? decodeURIComponent(frame.getAttribute("src") || "") : "";
return (tab && (tab.textContent || "").includes(needle))
|| (panel && (panel.textContent || "").includes(needle))
|| frameSrc.includes(needle);
},
text,
{ timeout: UI_TIMEOUT_MS },
);
await page.screenshot({ path: path.join(OUTPUT_DIR, `${label}.png`), fullPage: false });
}
async function createAdminGrant(root, targetUserId) {
const adminContext = await chromium.launch({
headless: true,
executablePath: CHROMIUM_EXECUTABLE_PATH,
}).then((browser) => browser.newContext({
extraHTTPHeaders: {
// 方案 Aops admin 为 mnote-admin;勿再用 mnote-e2e 冒充 admin
"x-mnote-actor-id": process.env.MNOTE_ADMIN_USERNAME || "mnote-admin",
"x-mnote-actor-type": "admin",
},
}).then((context) => ({ browser, context })));
try {
const response = await adminContext.context.request.post(`${BASE_URL}/api/admin/access-policy/grants`, {
data: {
userId: targetUserId,
rootUri: fileUrl(root),
permission: "read",
recursive: true,
capabilities: [],
},
});
assert(response.ok(), `创建外部授权失败: ${response.status()} ${await response.text()}`);
} finally {
await adminContext.context.close().catch(() => undefined);
await adminContext.browser.close().catch(() => undefined);
}
}
async function main() {
for (const fixture of [PDF_PATH, DOCX_PATH, PNG_PATH]) {
assert(fs.existsSync(fixture), `测试文件不存在: ${fixture}`);
}
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task506-attachments-"));
const allowedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task506-allowed-"));
const deniedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task506-denied-"));
const relativePath = "中文页面/中文页面.md";
const docDir = path.join(root, "中文页面");
fs.mkdirSync(docDir, { recursive: true });
writeWorkspaceManifest(root, "mnote-e2e");
fs.writeFileSync(path.join(root, relativePath), "# Attachment Matrix\n\n正文\n", "utf8");
fs.writeFileSync(path.join(docDir, "manual.pdf"), Buffer.from("%PDF-1.4\n% manual\n", "utf8"));
fs.writeFileSync(path.join(docDir, "manual.png"), fs.readFileSync(PNG_PATH));
fs.mkdirSync(path.join(root, "assets"), { recursive: true });
fs.writeFileSync(path.join(root, "assets", "bad.pdf"), Buffer.from("%PDF-1.4\n% bad\n", "utf8"));
fs.writeFileSync(path.join(allowedRoot, "allowed.pdf"), Buffer.from("%PDF-1.4\n% allowed\n", "utf8"));
fs.writeFileSync(path.join(deniedRoot, "denied.pdf"), Buffer.from("%PDF-1.4\n% denied\n", "utf8"));
const browser = await chromium.launch({
headless: process.env.HEADFUL !== "1",
executablePath: CHROMIUM_EXECUTABLE_PATH,
});
const context = await browser.newContext({
viewport: { width: 1366, height: 900 },
extraHTTPHeaders: {
"x-mnote-actor-id": "mnote-e2e",
"x-mnote-actor-type": "user",
},
});
await bypassRuntimeAssetCache(context);
let page = await context.newPage();
const result = { root, allowedRoot, deniedRoot, states: [], console: [], pageErrors: [] };
const attachPageDiagnostics = (targetPage) => {
targetPage.on("console", (message) => result.console.push({ type: message.type(), text: message.text() }));
targetPage.on("pageerror", (error) => result.pageErrors.push(String(error && error.stack || error)));
};
attachPageDiagnostics(page);
try {
await ensureTestUser(context);
await quickLogin(page);
await createAdminGrant(allowedRoot, "mnote-e2e");
await openDocument(page, root, relativePath);
await uploadAttachmentViaSlash(page, PDF_PATH);
await page.waitForFunction(() => document.documentElement.getAttribute("data-mnote-last-upload-saved") === "true", null, { timeout: UI_TIMEOUT_MS });
await waitForNoUploadSaveError(page, "pdf-upload");
const pdfName = path.basename(PDF_PATH);
const immediatePdfLinks = await editorAttachmentLinks(page);
assert(
immediatePdfLinks.some((link) => link.text.includes(pdfName) && link.missing !== "true"),
`中文页面上传 PDF 后刷新前不应标记 missing: ${JSON.stringify(immediatePdfLinks)}`,
);
result.states.push({ step: "click-uploaded-pdf-before-reload", click: await clickAttachmentLink(page, pdfName) });
await waitForActiveResource(page, pdfName, "00-uploaded-pdf-open-before-reload");
const immediatePdfOpenUrl = await page.evaluate(() => {
const panel = document.querySelector('.mnote-resource-tab-panel:not([hidden])');
const frame = panel?.querySelector?.('iframe.mnote-resource-tab-frame');
return frame instanceof HTMLIFrameElement ? decodeURIComponent(frame.getAttribute("src") || "") : "";
});
assert(
!immediatePdfOpenUrl.includes("~E"),
`中文 local-md 解码后不应把 ~E6... 写入 PDF open URL: ${immediatePdfOpenUrl}`,
);
await openDocument(page, root, relativePath);
await uploadAttachmentViaSlash(page, PNG_PATH);
await waitForNoUploadSaveError(page, "png-upload");
await openDocument(page, root, relativePath);
await uploadAttachmentViaSlash(page, DOCX_PATH);
await waitForNoUploadSaveError(page, "docx-upload");
let markdown = readMarkdown(root, relativePath);
assertNoRuntimeHref(markdown, "after-uploads");
assert(/\[[^\]]+\.pdf\]\(\.\/[^)]+\.pdf\)/i.test(markdown), `应保存标准 PDF 相对链接\n${markdown}`);
assert(/\[[^\]]+\.docx\]\(\.\/[^)]+\.docx\)/i.test(markdown), `应保存标准 DOCX 相对链接\n${markdown}`);
assert(/!\[[^\]]*\]\(\.\/[^)]+\.png\)/i.test(markdown), `应保存标准 PNG 图片链接\n${markdown}`);
assert(fs.existsSync(path.join(docDir, path.basename(PDF_PATH))), "PDF 应写入 md 同目录");
assert(fs.existsSync(path.join(docDir, path.basename(DOCX_PATH))), "DOCX 应写入 md 同目录");
assert(fs.existsSync(path.join(docDir, path.basename(PNG_PATH))), "PNG 应写入 md 同目录");
assert(!fs.existsSync(path.join(docDir, "Loose", path.basename(PDF_PATH))), "非 bundle md 不应写入同名子目录");
const manualAppend = [
"",
`[同目录手写](./manual.pdf)`,
`![手写图片](./manual.png)`,
`[授权外部](file://${path.join(allowedRoot, "allowed.pdf")})`,
`[未授权外部](file://${path.join(deniedRoot, "denied.pdf")})`,
`[不支持上级目录](../assets/bad.pdf)`,
"",
].join("\n");
fs.appendFileSync(path.join(root, relativePath), manualAppend, "utf8");
await openDocument(page, root, relativePath);
const aggregate = await readAggregate(page);
const refs = Array.isArray(aggregate?.body?.attachmentRefs) ? aggregate.body.attachmentRefs : [];
const byLabel = (label) => refs.find((ref) => ref.label === label);
assert.equal(byLabel("同目录手写")?.authorized, true, `同目录手写应授权: ${JSON.stringify(refs)}`);
assert.equal(byLabel("授权外部")?.authorized, true, `授权外部应授权: ${JSON.stringify(refs)}`);
assert.equal(byLabel("未授权外部")?.authorized, false, `未授权外部应阻断: ${JSON.stringify(refs)}`);
assert.equal(byLabel("不支持上级目录")?.kind, "unknown", `../assets 应为 unknown: ${JSON.stringify(refs)}`);
result.states.push({ step: "click-manual", click: await clickAttachmentLink(page, "同目录手写") });
await waitForActiveResource(page, "manual.pdf", "01-manual-pdf-open");
result.states.push({ step: "click-authorized-external", click: await clickAttachmentLink(page, "授权外部") });
await waitForActiveResource(page, "allowed.pdf", "02-authorized-file-open");
result.states.push({ step: "click-unauthorized-external", click: await clickAttachmentLink(page, "未授权外部") });
await page.waitForFunction(() => document.documentElement.getAttribute("data-mnote-attachment-open-blocked") === "unauthorized", null, { timeout: UI_TIMEOUT_MS });
await page.screenshot({ path: path.join(OUTPUT_DIR, "03-unauthorized-blocked.png"), fullPage: false });
fs.unlinkSync(path.join(docDir, pdfName));
await openDocument(page, root, relativePath);
await page.waitForFunction(
(name) => Array.from(document.querySelectorAll('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a'))
.some((link) => (link.textContent || "").includes(name)
&& (link.getAttribute("data-mnote-attachment-missing") === "true"
|| link.classList.contains("mnote-uploaded-attachment-missing"))),
pdfName,
{ timeout: UI_TIMEOUT_MS },
);
result.states.push({ step: "click-missing-uploaded-pdf", click: await clickAttachmentLink(page, pdfName) });
await page.waitForFunction(() => document.documentElement.getAttribute("data-mnote-attachment-open-blocked") === "missing", null, { timeout: UI_TIMEOUT_MS });
await page.screenshot({ path: path.join(OUTPUT_DIR, "04-missing-blocked.png"), fullPage: false });
await openDocument(page, root, relativePath);
const finalLinks = await editorAttachmentLinks(page);
markdown = readMarkdown(root, relativePath);
assertNoRuntimeHref(markdown, "final-markdown");
result.states.push({ step: "final", markdown, refs, links: finalLinks });
fs.writeFileSync(RESULT_PATH, `${JSON.stringify({ ok: true, ...result }, null, 2)}\n`, "utf8");
console.log(JSON.stringify({ ok: true, resultPath: RESULT_PATH, root, allowedRoot, deniedRoot }, null, 2));
} catch (error) {
const diagnostics = await page.evaluate(() => ({
url: location.href,
blocked: document.documentElement.getAttribute("data-mnote-attachment-open-blocked") || "",
links: Array.from(document.querySelectorAll('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror a')).map((link) => ({
text: link.textContent || "",
href: link.getAttribute("href") || "",
className: link.getAttribute("class") || "",
missing: link.getAttribute("data-mnote-attachment-missing") || "",
unauthorized: link.getAttribute("data-mnote-attachment-unauthorized") || "",
})),
aggregate: (() => {
const script = document.getElementById("__MNOTE_PAGE_AGGREGATE__");
return script ? JSON.parse(script.textContent || "null") : null;
})(),
})).catch((err) => ({ diagnosticsError: String(err) }));
await page.screenshot({ path: path.join(OUTPUT_DIR, "failure.png"), fullPage: true }).catch(() => undefined);
fs.writeFileSync(
RESULT_PATH,
`${JSON.stringify({ ok: false, ...result, diagnostics, error: String(error && error.stack || error) }, null, 2)}\n`,
"utf8",
);
console.error(JSON.stringify({ ok: false, resultPath: RESULT_PATH, root, diagnostics, error: String(error && error.stack || error) }, null, 2));
process.exitCode = 1;
} finally {
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});