238 lines
8.3 KiB
JavaScript
238 lines
8.3 KiB
JavaScript
"use strict";
|
||||
|
|
|
|||
|
|
const { chromium } = require("playwright");
|
|||
|
|
const {
|
|||
|
|
BASE_URL,
|
|||
|
|
UI_TIMEOUT_MS,
|
|||
|
|
assert,
|
|||
|
|
createTempDocument,
|
|||
|
|
ensureAuthenticated,
|
|||
|
|
purgeDocument,
|
|||
|
|
} = require("./tree-shell-smoke-helpers");
|
|||
|
|
|
|||
|
|
function readDocumentIdFromUrl(url) {
|
|||
|
|
try {
|
|||
|
|
const parsed = new URL(url);
|
|||
|
|
const match = parsed.pathname.match(/^\/documents\/([^/]+)$/);
|
|||
|
|
return match ? match[1] : null;
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function collectRuntimeIslandDiagnostics(page) {
|
|||
|
|
return page.evaluate(() => {
|
|||
|
|
const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|||
|
|
const observability = document.querySelector("[data-editor-host-observability]");
|
|||
|
|
const editor = host?.querySelector(".editor-surface .ProseMirror");
|
|||
|
|
const textareaCount = host?.querySelectorAll("textarea").length ?? 0;
|
|||
|
|
const contenteditableCount = host?.querySelectorAll(".editor-surface .ProseMirror[contenteditable]").length ?? 0;
|
|||
|
|
return {
|
|||
|
|
hostKind: host?.getAttribute("data-editor-host-kind") ?? null,
|
|||
|
|
runtimeStatus: host?.getAttribute("data-runtime-editor-status") ?? null,
|
|||
|
|
activeHostKind: observability?.getAttribute("data-editor-host-active") ?? null,
|
|||
|
|
observability: observability?.getAttribute("data-editor-host-observability") ?? null,
|
|||
|
|
editorTagName: editor instanceof HTMLElement ? editor.tagName : null,
|
|||
|
|
editorIsContentEditable: editor instanceof HTMLElement ? editor.isContentEditable : false,
|
|||
|
|
editorContentEditableAttr: editor instanceof HTMLElement ? editor.getAttribute("contenteditable") : null,
|
|||
|
|
editorCount: host?.querySelectorAll(".editor-surface .ProseMirror").length ?? 0,
|
|||
|
|
textareaCount,
|
|||
|
|
contenteditableCount,
|
|||
|
|
hostHTML: host?.innerHTML?.slice(0, 2000) ?? null,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function waitForRuntimeIsland(page) {
|
|||
|
|
const root = page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|||
|
|
await root.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|||
|
|
try {
|
|||
|
|
await page.waitForFunction(
|
|||
|
|
() => {
|
|||
|
|
const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
|||
|
|
const observability = document.querySelector("[data-editor-host-observability]");
|
|||
|
|
const editor = host?.querySelector(".editor-surface .ProseMirror");
|
|||
|
|
return (
|
|||
|
|
host?.getAttribute("data-editor-host-kind") === "leptos_tiptap_island" &&
|
|||
|
|
observability?.getAttribute("data-editor-host-active") === "leptos_tiptap_island" &&
|
|||
|
|
host?.getAttribute("data-runtime-editor-status") !== "error" &&
|
|||
|
|
editor instanceof HTMLElement &&
|
|||
|
|
editor.isContentEditable === true
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
null,
|
|||
|
|
{ timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
} catch (error) {
|
|||
|
|
const diagnostics = await collectRuntimeIslandDiagnostics(page).catch(() => null);
|
|||
|
|
throw new Error(
|
|||
|
|
`${error instanceof Error ? error.message : String(error)}\n${JSON.stringify(diagnostics, null, 2)}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const editorCount = await root.locator(".editor-surface .ProseMirror").count();
|
|||
|
|
const textareaCount = await root.locator("textarea").count();
|
|||
|
|
const contenteditableCount = await root.locator(".editor-surface .ProseMirror[contenteditable]").count();
|
|||
|
|
if (editorCount === 0 || contenteditableCount === 0 || textareaCount > 0) {
|
|||
|
|
const diagnostics = await collectRuntimeIslandDiagnostics(page).catch(() => null);
|
|||
|
|
throw new Error(
|
|||
|
|
[
|
|||
|
|
editorCount === 0 ? "island 主编辑器根节点内缺少 `.editor-surface .ProseMirror` surface" : null,
|
|||
|
|
contenteditableCount === 0 ? "island 主编辑器 surface 未暴露真实 contenteditable" : null,
|
|||
|
|
textareaCount > 0 ? "island 主编辑器根节点内不应回退为 textarea" : null,
|
|||
|
|
]
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.join(";") +
|
|||
|
|
`\n${JSON.stringify(diagnostics, null, 2)}`,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function waitForSaved(page) {
|
|||
|
|
await page.waitForFunction(
|
|||
|
|
() =>
|
|||
|
|
document
|
|||
|
|
.querySelector('[data-editor-host-kind="leptos_tiptap_island"]')
|
|||
|
|
?.getAttribute("data-runtime-editor-status") === "saved",
|
|||
|
|
null,
|
|||
|
|
{ timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 main() {
|
|||
|
|
const browser = await chromium.launch({ headless: true });
|
|||
|
|
const context = await browser.newContext({
|
|||
|
|
viewport: { width: 1440, height: 960 },
|
|||
|
|
});
|
|||
|
|
const page = await context.newPage();
|
|||
|
|
|
|||
|
|
let caughtError = null;
|
|||
|
|
let createdDocumentId = null;
|
|||
|
|
let createdWorkspaceId = null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await ensureAuthenticated(page, context.request);
|
|||
|
|
const created = await createTempDocument(context.request, null);
|
|||
|
|
createdDocumentId = created.documentId;
|
|||
|
|
createdWorkspaceId = created.workspaceId;
|
|||
|
|
|
|||
|
|
await page.goto(
|
|||
|
|
`${BASE_URL}/documents/${createdDocumentId}?workspaceId=${encodeURIComponent(createdWorkspaceId)}`,
|
|||
|
|
{ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
assert(
|
|||
|
|
page.url().includes(`/documents/${createdDocumentId}`),
|
|||
|
|
`未进入新建页面:${page.url()}`,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await waitForRuntimeIsland(page);
|
|||
|
|
|
|||
|
|
const editor = page
|
|||
|
|
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
|||
|
|
.first();
|
|||
|
|
const text = `task108-island-${Date.now().toString().slice(-6)}`;
|
|||
|
|
|
|||
|
|
await editor.evaluate((el) => {
|
|||
|
|
if (el instanceof HTMLElement) {
|
|||
|
|
el.focus();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
await page.keyboard.type(text, { delay: 30 });
|
|||
|
|
await waitForSaved(page);
|
|||
|
|
assert((await readEditorText(page)).includes(text), "默认 runtime island 未写入文本");
|
|||
|
|
|
|||
|
|
await page.keyboard.press("Control+z");
|
|||
|
|
await page.waitForFunction(
|
|||
|
|
(expected) => {
|
|||
|
|
const editorNode = document.querySelector(
|
|||
|
|
'[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]',
|
|||
|
|
);
|
|||
|
|
return !(editorNode?.textContent ?? "").includes(expected);
|
|||
|
|
},
|
|||
|
|
text,
|
|||
|
|
{ timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
await page.keyboard.press("Control+y");
|
|||
|
|
await page.waitForFunction(
|
|||
|
|
(expected) => {
|
|||
|
|
const editorNode = document.querySelector(
|
|||
|
|
'[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]',
|
|||
|
|
);
|
|||
|
|
return (editorNode?.textContent ?? "").includes(expected);
|
|||
|
|
},
|
|||
|
|
text,
|
|||
|
|
{ timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
await waitForSaved(page);
|
|||
|
|
|
|||
|
|
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
|||
|
|
await waitForRuntimeIsland(page);
|
|||
|
|
assert((await readEditorText(page)).includes(text), "刷新后未回填 runtime island 保存内容");
|
|||
|
|
|
|||
|
|
await page.goto(
|
|||
|
|
`${BASE_URL}/documents/${createdDocumentId}?workspaceId=${encodeURIComponent(createdWorkspaceId)}&editorHost=blocknote`,
|
|||
|
|
{ waitUntil: "commit", timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
await page.waitForFunction(
|
|||
|
|
() =>
|
|||
|
|
document
|
|||
|
|
.querySelector("[data-editor-host-observability]")
|
|||
|
|
?.getAttribute("data-editor-host-active") === "blocknote",
|
|||
|
|
null,
|
|||
|
|
{ timeout: UI_TIMEOUT_MS },
|
|||
|
|
);
|
|||
|
|
const runtimeIslandCount = await page
|
|||
|
|
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]')
|
|||
|
|
.count();
|
|||
|
|
assert(runtimeIslandCount === 0, "显式 blocknote 回退下不应继续挂载 island 主编辑器");
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
JSON.stringify(
|
|||
|
|
{
|
|||
|
|
ok: true,
|
|||
|
|
baseUrl: BASE_URL,
|
|||
|
|
documentId: createdDocumentId,
|
|||
|
|
workspaceId: createdWorkspaceId,
|
|||
|
|
text,
|
|||
|
|
},
|
|||
|
|
null,
|
|||
|
|
2,
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
} catch (error) {
|
|||
|
|
caughtError = error;
|
|||
|
|
} finally {
|
|||
|
|
if (createdDocumentId) {
|
|||
|
|
try {
|
|||
|
|
await purgeDocument(context.request, createdDocumentId);
|
|||
|
|
} 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);
|
|||
|
|
});
|