"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); });