Files

196 lines
7.2 KiB
JavaScript
Raw Permalink Normal View History

#!/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-body-sync-smoke");
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 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 typeIntoEditor(page, text) {
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: 20 });
}
async function waitForBodySync(requestContext, workspaceId, documentId, beforeRevision, beforeConflictKey, expectedText) {
const deadline = Date.now() + UI_TIMEOUT_MS;
let lastAggregate = null;
while (Date.now() < deadline) {
lastAggregate = await fetchPageAggregate(requestContext, workspaceId, documentId);
const body = lastAggregate.body ?? {};
const matchedBlock = findBlockWithText(lastAggregate, expectedText);
if (
matchedBlock &&
typeof body.revision === "number" &&
body.revision > beforeRevision &&
typeof body.conflictDetectionKey === "string" &&
body.conflictDetectionKey &&
body.conflictDetectionKey !== beforeConflictKey &&
body.blockDocument?.documentId === documentId
) {
return { aggregate: lastAggregate, matchedBlock };
}
await new Promise((resolve) => setTimeout(resolve, 350));
}
throw new Error(
`等待 Page Aggregate body 同步超时:${JSON.stringify({
documentId,
beforeRevision,
beforeConflictKey,
lastRevision: lastAggregate?.body?.revision ?? null,
lastConflictDetectionKey: lastAggregate?.body?.conflictDetectionKey ?? null,
hasExpectedBlock: Boolean(lastAggregate && findBlockWithText(lastAggregate, expectedText)),
})}`,
);
}
async function main() {
await fs.mkdir(OUT_DIR, { recursive: true });
const suffix = Date.now().toString(36);
const expectedText = `Page Aggregate body sync ${suffix}`;
const evidencePath = path.join(OUT_DIR, `${suffix}.json`);
const screenshotPath = path.join(OUT_DIR, `${suffix}.png`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
const page = await context.newPage();
const saveRequests = [];
const createdIds = [];
page.on("request", (request) => {
if (!request.url().includes("/api/documents/save") || request.method() !== "POST") return;
const payload = request.postDataJSON();
saveRequests.push({
documentId: payload?.documentId ?? null,
workspaceId: payload?.workspaceId ?? null,
revision: payload?.revision ?? null,
conflictDetectionKey: payload?.conflictDetectionKey ?? null,
commandName: payload?.commandName ?? null,
});
});
try {
const viewer = await ensureAuthenticated(page, context.request);
const target = await createTempDocument(context.request, null);
createdIds.push(target.documentId);
const before = await fetchPageAggregate(context.request, target.workspaceId, target.documentId);
const beforeRevision = typeof before.body?.revision === "number" ? before.body.revision : -1;
const beforeConflictKey = typeof before.body?.conflictDetectionKey === "string" ? before.body.conflictDetectionKey : "";
await openDocument(page, target.workspaceId, target.documentId);
await waitForRuntimeIsland(page);
await typeIntoEditor(page, expectedText);
const saveResponse = await 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 === target.documentId && response.ok();
},
{ timeout: UI_TIMEOUT_MS },
);
const { aggregate: after, matchedBlock } = await waitForBodySync(
context.request,
target.workspaceId,
target.documentId,
beforeRevision,
beforeConflictKey,
expectedText,
);
await page.screenshot({ path: screenshotPath, fullPage: false });
const evidence = {
ok: true,
baseUrl: BASE_URL,
viewerUserId: viewer.userId,
workspaceId: target.workspaceId,
documentId: target.documentId,
expectedText,
before: {
revision: before.body?.revision ?? null,
conflictDetectionKey: before.body?.conflictDetectionKey ?? null,
blockCount: before.body?.blockDocument?.blocks?.length ?? null,
},
after: {
revision: after.body?.revision ?? null,
conflictDetectionKey: after.body?.conflictDetectionKey ?? null,
blockProjectionVersion: after.body?.blockProjectionVersion ?? null,
projectionSource: after.body?.projectionSource ?? null,
blockCount: after.body?.blockDocument?.blocks?.length ?? null,
},
matchedBlock: {
blockId: matchedBlock.blockId,
type: matchedBlock.type,
text: matchedBlock.text,
revisionRef: matchedBlock.revisionRef,
editable: matchedBlock.editable,
},
saveResponseStatus: saveResponse.status(),
saveRequests,
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);
});