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