523 lines
22 KiB
JavaScript
523 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert");
|
|
const fs = require("node:fs/promises");
|
|
const path = require("node:path");
|
|
const { chromium } = require("playwright");
|
|
const {
|
|
BASE_URL,
|
|
UI_TIMEOUT_MS,
|
|
cleanupDocuments,
|
|
createTempDocument,
|
|
ensureAuthenticated,
|
|
openDocument,
|
|
requestJson,
|
|
} = require("./tree-shell-smoke-helpers");
|
|
|
|
const OUT_DIR = path.join(process.cwd(), "tmp", "page-aggregate-refresh-persistence-smoke");
|
|
const CHROME_EXECUTABLE = process.env.PLAYWRIGHT_CHROME_EXECUTABLE || "";
|
|
|
|
async function callMnoteTool(requestContext, payload) {
|
|
return requestJson(requestContext, "/api/hermes/tools/mnote/call", {
|
|
method: "POST",
|
|
headers: { "x-mnote-actor-id": payload.actorId || "smoke-user" },
|
|
data: payload,
|
|
});
|
|
}
|
|
|
|
async function fetchPageAggregate(requestContext, workspaceId, documentId) {
|
|
const payload = await requestJson(
|
|
requestContext,
|
|
`/api/page-aggregate/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
|
|
{ method: "GET" },
|
|
);
|
|
assert.equal(payload.schema, "mnote.page_aggregate.v1", "Page Aggregate schema 应保持稳定");
|
|
return payload.result;
|
|
}
|
|
|
|
function pageSubtreeSummary(aggregate) {
|
|
const subtree = aggregate?.tree?.pageSubtree ?? null;
|
|
return {
|
|
rootNodeId: subtree?.rootNodeId ?? null,
|
|
outlineLength: Array.isArray(subtree?.outline) ? subtree.outline.length : null,
|
|
};
|
|
}
|
|
|
|
async function fetchAiDoc(requestContext, target, actorId, suffix, label) {
|
|
const response = await callMnoteTool(requestContext, {
|
|
toolName: "mnote.doc.fetch",
|
|
workspaceId: target.workspaceId,
|
|
documentId: target.documentId,
|
|
actorId,
|
|
sessionId: `sess_page_aggregate_ai_fetch_${suffix}`,
|
|
runId: `run_page_aggregate_ai_fetch_${suffix}_${label}`,
|
|
toolCallId: `call_page_aggregate_ai_fetch_${suffix}_${label}`,
|
|
traceId: `trace_page_aggregate_ai_fetch_${suffix}_${label}`,
|
|
capabilityScope: ["page.read"],
|
|
args: {
|
|
scope: "full",
|
|
detail: "with_ids",
|
|
maxBlocks: 20,
|
|
},
|
|
});
|
|
assert.equal(response.ok, true, `${label}: mnote.doc.fetch 应成功`);
|
|
assert.equal(response.result.schema, "mnote.page_ai_context.v1", `${label}: AI fetch schema 应稳定`);
|
|
assert(Array.isArray(response.result.blocks), `${label}: AI fetch 应返回 blocks`);
|
|
return response.result;
|
|
}
|
|
|
|
function findBlockWithText(aggregate, text) {
|
|
const blocks = aggregate?.body?.blockDocument?.blocks;
|
|
if (!Array.isArray(blocks)) return null;
|
|
return blocks.find((block) => typeof block?.text === "string" && block.text.includes(text)) ?? null;
|
|
}
|
|
|
|
async function waitForRuntimeIsland(page) {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
const editor = host?.querySelector(".editor-surface .ProseMirror[contenteditable='true']");
|
|
return (
|
|
host?.getAttribute("data-editor-host-kind") === "leptos_tiptap_island" &&
|
|
host?.getAttribute("data-runtime-editor-status") !== "error" &&
|
|
editor instanceof HTMLElement &&
|
|
editor.isContentEditable
|
|
);
|
|
},
|
|
null,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
}
|
|
|
|
async function waitForPageTitleInput(page) {
|
|
const input = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
|
|
await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
return input;
|
|
}
|
|
|
|
async function readVisibleTitle(page) {
|
|
const titleInput = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
|
|
if (await titleInput.isVisible().catch(() => false)) {
|
|
return (await titleInput.inputValue()).trim();
|
|
}
|
|
const heading = page.locator("h1").first();
|
|
return ((await heading.textContent()) ?? "").trim();
|
|
}
|
|
|
|
async function readEditorText(page) {
|
|
return page.evaluate(() => {
|
|
const editor = document.querySelector(
|
|
'[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror',
|
|
);
|
|
return (editor?.textContent ?? "").trim();
|
|
});
|
|
}
|
|
|
|
async function renameThroughPageHead(page, documentId, title) {
|
|
const titleInput = await waitForPageTitleInput(page);
|
|
const responsePromise = page.waitForResponse(
|
|
async (response) => {
|
|
if (!response.url().includes("/api/documents/title") || response.request().method() !== "POST") {
|
|
return false;
|
|
}
|
|
const payload = response.request().postDataJSON();
|
|
return (
|
|
payload?.documentId === documentId &&
|
|
payload?.title === title &&
|
|
payload?.commandName === "page.head.updateTitle" &&
|
|
response.ok()
|
|
);
|
|
},
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
await titleInput.click({ timeout: UI_TIMEOUT_MS });
|
|
await page.keyboard.press("Control+a");
|
|
await page.keyboard.type(title, { delay: 10 });
|
|
await titleInput.blur();
|
|
const response = await responsePromise;
|
|
return response.status();
|
|
}
|
|
|
|
async function typeIntoEditor(page, documentId, text) {
|
|
const responsePromise = page.waitForResponse(
|
|
async (response) => {
|
|
if (!response.url().includes("/api/documents/save") || response.request().method() !== "POST") {
|
|
return false;
|
|
}
|
|
const payload = response.request().postDataJSON();
|
|
return payload?.documentId === documentId && response.ok();
|
|
},
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
const editor = page
|
|
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
|
.first();
|
|
await editor.click({ timeout: UI_TIMEOUT_MS });
|
|
await page.keyboard.press("Control+a");
|
|
await page.keyboard.press("Backspace");
|
|
await page.keyboard.type(text, { delay: 10 });
|
|
const response = await responsePromise;
|
|
return response.status();
|
|
}
|
|
|
|
async function openPageSettingsDialog(page) {
|
|
const trigger = page.getByTestId("wolai-page-settings-trigger");
|
|
await trigger.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
await trigger.click({ timeout: UI_TIMEOUT_MS });
|
|
await page.locator('[data-page-option-checkbox="wideLayout"]').waitFor({
|
|
state: "visible",
|
|
timeout: UI_TIMEOUT_MS,
|
|
});
|
|
}
|
|
|
|
async function waitForOptionsResponse(page, documentId, mutate) {
|
|
const responsePromise = page.waitForResponse(
|
|
async (response) => {
|
|
if (!response.url().includes("/api/ui/preferences") || response.request().method() !== "PUT") {
|
|
return false;
|
|
}
|
|
const payload = response.request().postDataJSON();
|
|
return payload?.documentId === documentId && response.ok();
|
|
},
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
await mutate();
|
|
const response = await responsePromise;
|
|
const payload = await response.json();
|
|
assert.equal(payload?.owner, "mnote-web", "页面设置写入必须由 mnote-web 持有");
|
|
assert(payload?.result?.pageOptions, "页面设置写入必须返回 SQLite 合并后的 pageOptions");
|
|
return response.status();
|
|
}
|
|
|
|
async function setPageOptionsThroughUi(page, documentId) {
|
|
await openPageSettingsDialog(page);
|
|
const wideStatus = await waitForOptionsResponse(page, documentId, async () => {
|
|
await page.locator('[data-page-option-checkbox="wideLayout"]').click({ timeout: UI_TIMEOUT_MS });
|
|
});
|
|
const smallStatus = await waitForOptionsResponse(page, documentId, async () => {
|
|
await page.locator('[data-page-option-checkbox="smallText"]').click({ timeout: UI_TIMEOUT_MS });
|
|
});
|
|
const densityStatus = await waitForOptionsResponse(page, documentId, async () => {
|
|
await page.locator('[data-page-settings-tab="custom"]').click({ timeout: UI_TIMEOUT_MS });
|
|
await page.locator('[data-page-option-select="layoutDensity"]').selectOption("compact");
|
|
});
|
|
return { wideStatus, smallStatus, densityStatus };
|
|
}
|
|
|
|
async function readRuntimePageOptions(page) {
|
|
return page.evaluate(() => {
|
|
const shell = document.querySelector(".document-shell");
|
|
const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
const editorSurface = editorRoot?.querySelector(".editor-surface");
|
|
const editor = editorRoot?.querySelector(".ProseMirror");
|
|
const firstParagraph = editorRoot?.querySelector(".ProseMirror p");
|
|
return {
|
|
htmlWide: document.documentElement.getAttribute("data-page-wide-layout"),
|
|
htmlSmall: document.documentElement.getAttribute("data-page-small-text"),
|
|
htmlDensity: document.documentElement.getAttribute("data-layout-density"),
|
|
shellWide: shell instanceof HTMLElement ? shell.getAttribute("data-page-wide-layout") : null,
|
|
shellSmall: shell instanceof HTMLElement ? shell.getAttribute("data-page-small-text") : null,
|
|
shellDensity: shell instanceof HTMLElement ? shell.getAttribute("data-layout-density") : null,
|
|
editorRootWide: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-wide-layout") : null,
|
|
editorRootSmall: editorRoot instanceof HTMLElement ? editorRoot.getAttribute("data-page-small-text") : null,
|
|
editorSurfaceDensity:
|
|
editorSurface instanceof HTMLElement ? editorSurface.getAttribute("data-layout-density") : null,
|
|
editorFontSize: editor instanceof HTMLElement ? window.getComputedStyle(editor).fontSize : null,
|
|
paragraphMarginBottom:
|
|
firstParagraph instanceof HTMLElement ? window.getComputedStyle(firstParagraph).marginBottom : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function readPageSettingsControls(page) {
|
|
await openPageSettingsDialog(page);
|
|
return {
|
|
wideLayout: await page.locator('[data-page-option-checkbox="wideLayout"]').isChecked(),
|
|
smallText: await page.locator('[data-page-option-checkbox="smallText"]').isChecked(),
|
|
layoutDensity: await page.locator('[data-page-option-select="layoutDensity"]').inputValue(),
|
|
};
|
|
}
|
|
|
|
async function waitForRuntimePageOptions(page) {
|
|
try {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const shell = document.querySelector(".document-shell");
|
|
const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|
const editorSurface = editorRoot?.querySelector(".editor-surface");
|
|
return (
|
|
document.documentElement.getAttribute("data-page-wide-layout") === "true" &&
|
|
document.documentElement.getAttribute("data-page-small-text") === "true" &&
|
|
document.documentElement.getAttribute("data-layout-density") === "compact" &&
|
|
shell?.getAttribute("data-page-wide-layout") === "true" &&
|
|
shell?.getAttribute("data-page-small-text") === "true" &&
|
|
shell?.getAttribute("data-layout-density") === "compact" &&
|
|
editorRoot?.getAttribute("data-page-wide-layout") === "true" &&
|
|
editorRoot?.getAttribute("data-page-small-text") === "true" &&
|
|
editorSurface?.getAttribute("data-layout-density") === "compact"
|
|
);
|
|
},
|
|
null,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
} catch (error) {
|
|
const runtime = await readRuntimePageOptions(page).catch((runtimeError) => ({
|
|
readRuntimeError: runtimeError instanceof Error ? runtimeError.message : String(runtimeError),
|
|
}));
|
|
const embedded = await page.evaluate(() => {
|
|
const node = document.getElementById("__MNOTE_PAGE_AGGREGATE__");
|
|
try {
|
|
return node?.textContent ? JSON.parse(node.textContent) : null;
|
|
} catch (parseError) {
|
|
return { parseError: parseError instanceof Error ? parseError.message : String(parseError) };
|
|
}
|
|
}).catch((embeddedError) => ({
|
|
readEmbeddedError: embeddedError instanceof Error ? embeddedError.message : String(embeddedError),
|
|
}));
|
|
throw new Error(
|
|
`等待 runtime page options 同步超时:${JSON.stringify({ runtime, embeddedPageOptions: embedded?.layout?.pageOptions ?? null })}\n${
|
|
error instanceof Error ? error.stack || error.message : String(error)
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function waitForAggregateState(requestContext, workspaceId, documentId, expectedTitle, expectedText) {
|
|
const deadline = Date.now() + UI_TIMEOUT_MS;
|
|
let aggregate = null;
|
|
while (Date.now() < deadline) {
|
|
aggregate = await fetchPageAggregate(requestContext, workspaceId, documentId);
|
|
const options = aggregate?.layout?.pageOptions ?? {};
|
|
const matchedBlock = findBlockWithText(aggregate, expectedText);
|
|
if (
|
|
aggregate?.head?.title === expectedTitle &&
|
|
matchedBlock &&
|
|
options.wideLayout === true &&
|
|
options.smallText === true &&
|
|
options.layoutDensity === "compact"
|
|
) {
|
|
return { aggregate, matchedBlock };
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
}
|
|
throw new Error(
|
|
`等待 Page Aggregate 刷新持久态同步超时:${JSON.stringify({
|
|
documentId,
|
|
expectedTitle,
|
|
expectedText,
|
|
lastTitle: aggregate?.head?.title ?? null,
|
|
lastOptions: aggregate?.layout?.pageOptions ?? null,
|
|
hasExpectedBlock: Boolean(aggregate && findBlockWithText(aggregate, expectedText)),
|
|
})}`,
|
|
);
|
|
}
|
|
|
|
function assertRuntimeOptions(runtime, label) {
|
|
assert.equal(runtime.htmlWide, "true", `${label} html wideLayout 应保持 true`);
|
|
assert.equal(runtime.htmlSmall, "true", `${label} html smallText 应保持 true`);
|
|
assert.equal(runtime.htmlDensity, "compact", `${label} html layoutDensity 应保持 compact`);
|
|
assert.equal(runtime.editorRootWide, "true", `${label} island root wideLayout 应保持 true`);
|
|
assert.equal(runtime.editorRootSmall, "true", `${label} island root smallText 应保持 true`);
|
|
assert.equal(runtime.editorSurfaceDensity, "compact", `${label} island surface density 应保持 compact`);
|
|
}
|
|
|
|
async function main() {
|
|
await fs.mkdir(OUT_DIR, { recursive: true });
|
|
const suffix = Date.now().toString(36);
|
|
const title = `Page Aggregate refresh ${suffix}`;
|
|
const bodyText = `Page Aggregate refresh body ${suffix}`;
|
|
const evidencePath = path.join(OUT_DIR, `${suffix}.json`);
|
|
const screenshotPath = path.join(OUT_DIR, `${suffix}.png`);
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
...(CHROME_EXECUTABLE ? { executablePath: CHROME_EXECUTABLE } : {}),
|
|
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
});
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
|
const page = await context.newPage();
|
|
const createdIds = [];
|
|
|
|
try {
|
|
const viewer = await ensureAuthenticated(page, context.request);
|
|
const target = await createTempDocument(context.request, null);
|
|
createdIds.push(target.documentId);
|
|
|
|
await openDocument(page, target.workspaceId, target.documentId);
|
|
await waitForPageTitleInput(page);
|
|
await waitForRuntimeIsland(page);
|
|
|
|
const titleStatus = await renameThroughPageHead(page, target.documentId, title);
|
|
assert((await readVisibleTitle(page)).includes(title), "写入后页头标题应立即显示最新值");
|
|
|
|
const saveStatus = await typeIntoEditor(page, target.documentId, bodyText);
|
|
await page.waitForFunction(
|
|
(expectedText) => (document.querySelector(".editor-surface .ProseMirror")?.textContent ?? "").includes(expectedText),
|
|
bodyText,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
|
|
const optionStatuses = await setPageOptionsThroughUi(page, target.documentId);
|
|
await waitForRuntimePageOptions(page);
|
|
const beforeReloadRuntime = await readRuntimePageOptions(page);
|
|
assertRuntimeOptions(beforeReloadRuntime, "刷新前");
|
|
|
|
const { aggregate: beforeReloadAggregate, matchedBlock: beforeReloadBlock } = await waitForAggregateState(
|
|
context.request,
|
|
target.workspaceId,
|
|
target.documentId,
|
|
title,
|
|
bodyText,
|
|
);
|
|
const beforeReloadAiFetch = await fetchAiDoc(context.request, target, viewer.userId, suffix, "before_reload");
|
|
const beforeReloadAiBlock = beforeReloadAiFetch.blocks.find((block) => block.blockId === beforeReloadBlock.blockId);
|
|
assert.equal(beforeReloadAiFetch.revision, beforeReloadAggregate.body?.revision, "刷新前 AI fetch revision 应与 Page Aggregate 一致");
|
|
assert.equal(
|
|
beforeReloadAiFetch.conflictDetectionKey,
|
|
beforeReloadAggregate.body?.conflictDetectionKey,
|
|
"刷新前 AI fetch conflictDetectionKey 应与 Page Aggregate 一致",
|
|
);
|
|
assert.equal(beforeReloadAiBlock?.text, bodyText, "刷新前 AI fetch 应回读 Page Aggregate 正文块");
|
|
assert.equal(
|
|
beforeReloadAggregate.tree?.pageSubtree?.rootNodeId,
|
|
target.documentId,
|
|
"刷新前 Page Aggregate pageSubtree.rootNodeId 应等于 documentId",
|
|
);
|
|
|
|
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
|
await waitForPageTitleInput(page);
|
|
await waitForRuntimeIsland(page);
|
|
await page.waitForFunction(
|
|
(expectedText) => (document.querySelector(".editor-surface .ProseMirror")?.textContent ?? "").includes(expectedText),
|
|
bodyText,
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
);
|
|
await waitForRuntimePageOptions(page);
|
|
|
|
const afterReloadTitle = await readVisibleTitle(page);
|
|
const afterReloadEditorText = await readEditorText(page);
|
|
const afterReloadRuntime = await readRuntimePageOptions(page);
|
|
assert(afterReloadTitle.includes(title), "刷新后页头标题不应回退旧快照");
|
|
assert(afterReloadEditorText.includes(bodyText), "刷新后正文不应回退旧快照");
|
|
assertRuntimeOptions(afterReloadRuntime, "刷新后");
|
|
const afterReloadControls = await readPageSettingsControls(page);
|
|
assert.equal(afterReloadControls.wideLayout, true, "刷新后页面设置 wideLayout 控件应保持 true");
|
|
assert.equal(afterReloadControls.smallText, true, "刷新后页面设置 smallText 控件应保持 true");
|
|
assert.equal(afterReloadControls.layoutDensity, "compact", "刷新后页面设置 layoutDensity 控件应保持 compact");
|
|
|
|
const { aggregate: afterReloadAggregate, matchedBlock: afterReloadBlock } = await waitForAggregateState(
|
|
context.request,
|
|
target.workspaceId,
|
|
target.documentId,
|
|
title,
|
|
bodyText,
|
|
);
|
|
const afterReloadAiFetch = await fetchAiDoc(context.request, target, viewer.userId, suffix, "after_reload");
|
|
const afterReloadAiBlock = afterReloadAiFetch.blocks.find((block) => block.blockId === afterReloadBlock.blockId);
|
|
assert.equal(afterReloadAiFetch.revision, afterReloadAggregate.body?.revision, "刷新后 AI fetch revision 应与 Page Aggregate 一致");
|
|
assert.equal(
|
|
afterReloadAiFetch.conflictDetectionKey,
|
|
afterReloadAggregate.body?.conflictDetectionKey,
|
|
"刷新后 AI fetch conflictDetectionKey 应与 Page Aggregate 一致",
|
|
);
|
|
assert.equal(afterReloadAiBlock?.text, bodyText, "刷新后 AI fetch 应回读 Page Aggregate 正文块");
|
|
assert.equal(
|
|
afterReloadAggregate.tree?.pageSubtree?.rootNodeId,
|
|
target.documentId,
|
|
"刷新后 Page Aggregate pageSubtree.rootNodeId 应等于 documentId",
|
|
);
|
|
|
|
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
const evidence = {
|
|
ok: true,
|
|
baseUrl: BASE_URL,
|
|
viewerUserId: viewer.userId,
|
|
workspaceId: target.workspaceId,
|
|
documentId: target.documentId,
|
|
expected: {
|
|
title,
|
|
bodyText,
|
|
pageOptions: {
|
|
wideLayout: true,
|
|
smallText: true,
|
|
layoutDensity: "compact",
|
|
},
|
|
},
|
|
commandStatuses: {
|
|
titleStatus,
|
|
saveStatus,
|
|
...optionStatuses,
|
|
},
|
|
beforeReload: {
|
|
aggregate: {
|
|
title: beforeReloadAggregate.head?.title ?? null,
|
|
revision: beforeReloadAggregate.body?.revision ?? null,
|
|
conflictDetectionKey: beforeReloadAggregate.body?.conflictDetectionKey ?? null,
|
|
pageOptions: beforeReloadAggregate.layout?.pageOptions ?? null,
|
|
blockProjectionVersion: beforeReloadAggregate.body?.blockProjectionVersion ?? null,
|
|
pageSubtree: pageSubtreeSummary(beforeReloadAggregate),
|
|
},
|
|
matchedBlock: {
|
|
blockId: beforeReloadBlock.blockId,
|
|
text: beforeReloadBlock.text,
|
|
revisionRef: beforeReloadBlock.revisionRef,
|
|
},
|
|
aiFetch: {
|
|
schema: beforeReloadAiFetch.schema,
|
|
revision: beforeReloadAiFetch.revision,
|
|
conflictDetectionKey: beforeReloadAiFetch.conflictDetectionKey,
|
|
matchedBlock: {
|
|
blockId: beforeReloadAiBlock?.blockId ?? null,
|
|
text: beforeReloadAiBlock?.text ?? null,
|
|
revisionRef: beforeReloadAiBlock?.revisionRef ?? null,
|
|
},
|
|
},
|
|
runtime: beforeReloadRuntime,
|
|
},
|
|
afterReload: {
|
|
aggregate: {
|
|
title: afterReloadAggregate.head?.title ?? null,
|
|
revision: afterReloadAggregate.body?.revision ?? null,
|
|
conflictDetectionKey: afterReloadAggregate.body?.conflictDetectionKey ?? null,
|
|
pageOptions: afterReloadAggregate.layout?.pageOptions ?? null,
|
|
blockProjectionVersion: afterReloadAggregate.body?.blockProjectionVersion ?? null,
|
|
pageSubtree: pageSubtreeSummary(afterReloadAggregate),
|
|
},
|
|
matchedBlock: {
|
|
blockId: afterReloadBlock.blockId,
|
|
text: afterReloadBlock.text,
|
|
revisionRef: afterReloadBlock.revisionRef,
|
|
},
|
|
aiFetch: {
|
|
schema: afterReloadAiFetch.schema,
|
|
revision: afterReloadAiFetch.revision,
|
|
conflictDetectionKey: afterReloadAiFetch.conflictDetectionKey,
|
|
matchedBlock: {
|
|
blockId: afterReloadAiBlock?.blockId ?? null,
|
|
text: afterReloadAiBlock?.text ?? null,
|
|
revisionRef: afterReloadAiBlock?.revisionRef ?? null,
|
|
},
|
|
},
|
|
title: afterReloadTitle,
|
|
editorText: afterReloadEditorText,
|
|
runtime: afterReloadRuntime,
|
|
controls: afterReloadControls,
|
|
},
|
|
screenshotPath,
|
|
evidencePath,
|
|
};
|
|
await fs.writeFile(evidencePath, JSON.stringify(evidence, null, 2), "utf8");
|
|
console.log(JSON.stringify(evidence, null, 2));
|
|
} finally {
|
|
await cleanupDocuments(context.request, createdIds).catch(() => undefined);
|
|
await page.close().catch(() => undefined);
|
|
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);
|
|
});
|