218 lines
6.9 KiB
JavaScript
218 lines
6.9 KiB
JavaScript
"use strict";
|
|||
|
|
|
||
|
|
const { chromium } = require("playwright");
|
||
|
|
const {
|
||
|
|
BASE_URL,
|
||
|
|
UI_TIMEOUT_MS,
|
||
|
|
assert,
|
||
|
|
createTempDocument,
|
||
|
|
ensureAuthenticated,
|
||
|
|
purgeDocument,
|
||
|
|
} = require("./tree-shell-smoke-helpers");
|
||
|
|
|
||
|
|
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 waitForSaved(page) {
|
||
|
|
await page.waitForFunction(
|
||
|
|
() =>
|
||
|
|
document
|
||
|
|
.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
|
||
|
|
?.getAttribute("data-runtime-editor-status") === "saved",
|
||
|
|
null,
|
||
|
|
{ timeout: UI_TIMEOUT_MS },
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function waitForTextPresent(page, text) {
|
||
|
|
await page.waitForFunction(
|
||
|
|
(expected) => {
|
||
|
|
const editor = document.querySelector(
|
||
|
|
'[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror',
|
||
|
|
);
|
||
|
|
return (editor?.textContent ?? "").includes(expected);
|
||
|
|
},
|
||
|
|
text,
|
||
|
|
{ timeout: UI_TIMEOUT_MS },
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function waitForPersisted(page, saveRequests, documentId, expectedText) {
|
||
|
|
const deadline = Date.now() + UI_TIMEOUT_MS;
|
||
|
|
while (Date.now() < deadline) {
|
||
|
|
const editorText = await readEditorText(page);
|
||
|
|
const runtimeStatus = await page.evaluate(() => {
|
||
|
|
return (
|
||
|
|
document
|
||
|
|
.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
|
||
|
|
?.getAttribute("data-runtime-editor-status") ?? null
|
||
|
|
);
|
||
|
|
});
|
||
|
|
const hasSaveRequest = saveRequests.some((item) => item.documentId === documentId);
|
||
|
|
if (editorText.includes(expectedText) && (runtimeStatus === "saved" || hasSaveRequest)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
await page.waitForTimeout(250);
|
||
|
|
}
|
||
|
|
throw new Error(`等待持久化超时:${documentId}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
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: 30 });
|
||
|
|
}
|
||
|
|
|
||
|
|
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 ?? "";
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function openDocument(page, documentId, workspaceId) {
|
||
|
|
await page.goto(
|
||
|
|
`${BASE_URL}/documents/${documentId}?workspaceId=${encodeURIComponent(workspaceId)}`,
|
||
|
|
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
||
|
|
);
|
||
|
|
await waitForRuntimeIsland(page);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
const browser = await chromium.launch({ headless: true });
|
||
|
|
const context = await browser.newContext({
|
||
|
|
viewport: { width: 1440, height: 960 },
|
||
|
|
});
|
||
|
|
const page = await context.newPage();
|
||
|
|
|
||
|
|
let docA = null;
|
||
|
|
let docB = null;
|
||
|
|
let caughtError = null;
|
||
|
|
const saveRequests = [];
|
||
|
|
|
||
|
|
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,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
await ensureAuthenticated(page, context.request);
|
||
|
|
docA = await createTempDocument(context.request, null);
|
||
|
|
docB = await createTempDocument(context.request, null);
|
||
|
|
|
||
|
|
const textA = `doc-a-${Date.now().toString().slice(-6)}`;
|
||
|
|
const textB = `doc-b-${(Date.now() + 1).toString().slice(-6)}`;
|
||
|
|
|
||
|
|
await openDocument(page, docA.documentId, docA.workspaceId);
|
||
|
|
await typeIntoEditor(page, textA);
|
||
|
|
await waitForTextPresent(page, textA);
|
||
|
|
await waitForPersisted(page, saveRequests, docA.documentId, textA);
|
||
|
|
assert((await readEditorText(page)).includes(textA), "A 页文本未进入编辑区");
|
||
|
|
|
||
|
|
await openDocument(page, docB.documentId, docB.workspaceId);
|
||
|
|
await typeIntoEditor(page, textB);
|
||
|
|
await waitForTextPresent(page, textB);
|
||
|
|
await waitForPersisted(page, saveRequests, docB.documentId, textB);
|
||
|
|
assert((await readEditorText(page)).includes(textB), "B 页文本未进入编辑区");
|
||
|
|
|
||
|
|
await openDocument(page, docA.documentId, docA.workspaceId);
|
||
|
|
assert((await readEditorText(page)).includes(textA), "切回 A 页后内容串页或丢失");
|
||
|
|
|
||
|
|
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||
|
|
await waitForRuntimeIsland(page);
|
||
|
|
assert((await readEditorText(page)).includes(textA), "A 页刷新后未回填");
|
||
|
|
|
||
|
|
await openDocument(page, docB.documentId, docB.workspaceId);
|
||
|
|
assert((await readEditorText(page)).includes(textB), "切回 B 页后内容串页或丢失");
|
||
|
|
|
||
|
|
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||
|
|
await waitForRuntimeIsland(page);
|
||
|
|
assert((await readEditorText(page)).includes(textB), "B 页刷新后未回填");
|
||
|
|
|
||
|
|
const savedDocumentIds = new Set(saveRequests.map((item) => item.documentId).filter(Boolean));
|
||
|
|
assert(savedDocumentIds.has(docA.documentId), "未观察到 A 页保存请求");
|
||
|
|
assert(savedDocumentIds.has(docB.documentId), "未观察到 B 页保存请求");
|
||
|
|
assert(
|
||
|
|
saveRequests.some(
|
||
|
|
(item) => item.documentId === docA.documentId && item.workspaceId === docA.workspaceId,
|
||
|
|
),
|
||
|
|
"A 页保存请求缺少正确 workspaceId/documentId 绑定",
|
||
|
|
);
|
||
|
|
assert(
|
||
|
|
saveRequests.some(
|
||
|
|
(item) => item.documentId === docB.documentId && item.workspaceId === docB.workspaceId,
|
||
|
|
),
|
||
|
|
"B 页保存请求缺少正确 workspaceId/documentId 绑定",
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log(
|
||
|
|
JSON.stringify(
|
||
|
|
{
|
||
|
|
ok: true,
|
||
|
|
docA,
|
||
|
|
docB,
|
||
|
|
saveRequests,
|
||
|
|
textA,
|
||
|
|
textB,
|
||
|
|
},
|
||
|
|
null,
|
||
|
|
2,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
caughtError = error;
|
||
|
|
} finally {
|
||
|
|
for (const doc of [docA, docB]) {
|
||
|
|
if (!doc?.documentId) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
await purgeDocument(context.request, doc.documentId);
|
||
|
|
} catch (cleanupError) {
|
||
|
|
if (!caughtError) {
|
||
|
|
caughtError = cleanupError;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
await page.close().catch(() => undefined);
|
||
|
|
await context.close().catch(() => undefined);
|
||
|
|
await browser.close().catch(() => undefined);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (caughtError) {
|
||
|
|
throw caughtError;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((error) => {
|
||
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||
|
|
process.exit(1);
|
||
|
|
});
|