feat(editor): save leptos island and page aggregate alignment progress
- switch main document flow toward leptos tiptap island host and generated runtime assets - align page aggregate loading, page head single-source updates, and AI tool result recovery - add tests and smoke scripts for title sync, AI route recovery, and editor host cutover
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
"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);
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
"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);
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
const {
|
||||
BASE_URL,
|
||||
UI_TIMEOUT_MS,
|
||||
assert,
|
||||
cleanupDocuments,
|
||||
createTempDocument,
|
||||
ensureAuthenticated,
|
||||
openDocument,
|
||||
openFilesystemView,
|
||||
openSectionView,
|
||||
renameDocument,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function waitForPageTitleInput(page) {
|
||||
const input = page.getByLabel("页面标题");
|
||||
await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
return input;
|
||||
}
|
||||
|
||||
async function waitForTitleSave(page, documentId, expectedTitle) {
|
||||
await page.waitForResponse(
|
||||
async (response) => {
|
||||
if (!response.url().includes("/api/documents/title") || response.request().method() !== "POST") {
|
||||
return false;
|
||||
}
|
||||
const payload = response.request().postDataJSON();
|
||||
return payload?.documentId === documentId && payload?.title === expectedTitle && response.ok();
|
||||
},
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function renameThroughPageHead(page, documentId, title) {
|
||||
const titleInput = await waitForPageTitleInput(page);
|
||||
await titleInput.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.keyboard.press("Control+a");
|
||||
await page.keyboard.type(title, { delay: 20 });
|
||||
await titleInput.blur();
|
||||
await waitForTitleSave(page, documentId, title);
|
||||
}
|
||||
|
||||
async function waitForBreadcrumbTitle(page, title) {
|
||||
await page.waitForFunction(
|
||||
(expectedTitle) => {
|
||||
const nav = document.querySelector("header nav");
|
||||
return (nav?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
title,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForSidebarRowTitle(page, documentId, title) {
|
||||
await page.waitForFunction(
|
||||
({ docId, expectedTitle }) => {
|
||||
const row = document.querySelector(`aside a[href="/documents/${docId}"]`);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
{ docId: documentId, expectedTitle: title },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForPageTreeTitle(page, documentId, title) {
|
||||
await openSectionView(page);
|
||||
const shellHost = page.getByTestId("sidebar-page-tree-shell");
|
||||
await shellHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
({ nodeId, expectedTitle }) => {
|
||||
const row = document.querySelector(
|
||||
`[data-testid="page-tree-row"][data-node-id="${nodeId}"]`,
|
||||
);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
{ nodeId: documentId, expectedTitle: title },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFileTreeTitle(page, documentId, title) {
|
||||
await openFilesystemView(page);
|
||||
const shellHost = page.getByTestId("sidebar-file-tree-shell");
|
||||
await shellHost.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(
|
||||
({ docId, expectedTitle }) => {
|
||||
const row = document.querySelector(
|
||||
`[data-testid="filetree-doc-row"][data-doc-id="${docId}"]`,
|
||||
);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
{ docId: documentId, expectedTitle: title },
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
async function readVisibleTitle(page) {
|
||||
const titleInput = page.getByLabel("页面标题");
|
||||
if (await titleInput.isVisible().catch(() => false)) {
|
||||
return (await titleInput.inputValue()).trim();
|
||||
}
|
||||
const heading = page.locator("h1").first();
|
||||
return ((await heading.textContent()) ?? "").trim();
|
||||
}
|
||||
|
||||
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 fixture = null;
|
||||
|
||||
try {
|
||||
const viewer = await ensureAuthenticated(page, context.request);
|
||||
const first = await createTempDocument(context.request, null);
|
||||
const second = await createTempDocument(context.request, null);
|
||||
const initialTitle = `phase-i-initial-${Date.now().toString().slice(-6)}`;
|
||||
const renamedTitle = `phase-i-renamed-${(Date.now() + 1).toString().slice(-6)}`;
|
||||
const siblingTitle = `phase-i-sibling-${(Date.now() + 2).toString().slice(-6)}`;
|
||||
|
||||
await renameDocument(context.request, first.workspaceId, first.documentId, initialTitle);
|
||||
await renameDocument(context.request, second.workspaceId, second.documentId, siblingTitle);
|
||||
|
||||
fixture = {
|
||||
workspaceId: first.workspaceId,
|
||||
primaryId: first.documentId,
|
||||
siblingId: second.documentId,
|
||||
createdIds: [first.documentId, second.documentId],
|
||||
initialTitle,
|
||||
renamedTitle,
|
||||
siblingTitle,
|
||||
};
|
||||
|
||||
await openDocument(page, fixture.workspaceId, fixture.primaryId);
|
||||
await waitForPageTitleInput(page);
|
||||
assert((await readVisibleTitle(page)).includes(initialTitle), "页头未回填初始标题");
|
||||
|
||||
await renameThroughPageHead(page, fixture.primaryId, fixture.renamedTitle);
|
||||
assert((await readVisibleTitle(page)).includes(fixture.renamedTitle), "页头未显示最新标题");
|
||||
|
||||
await waitForBreadcrumbTitle(page, fixture.renamedTitle);
|
||||
await waitForSidebarRowTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
await waitForPageTreeTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
await waitForFileTreeTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
|
||||
await openDocument(page, fixture.workspaceId, fixture.siblingId);
|
||||
await waitForPageTitleInput(page);
|
||||
assert((await readVisibleTitle(page)).includes(fixture.siblingTitle), "切到兄弟页后标题不正确");
|
||||
|
||||
await openDocument(page, fixture.workspaceId, fixture.primaryId);
|
||||
await waitForPageTitleInput(page);
|
||||
await waitForBreadcrumbTitle(page, fixture.renamedTitle);
|
||||
await waitForSidebarRowTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
assert((await readVisibleTitle(page)).includes(fixture.renamedTitle), "切页往返后页头标题回闪");
|
||||
|
||||
await page.reload({ waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||||
await waitForPageTitleInput(page);
|
||||
await waitForBreadcrumbTitle(page, fixture.renamedTitle);
|
||||
await waitForSidebarRowTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
await waitForPageTreeTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
await waitForFileTreeTitle(page, fixture.primaryId, fixture.renamedTitle);
|
||||
assert((await readVisibleTitle(page)).includes(fixture.renamedTitle), "刷新后页头标题未保持一致");
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
baseUrl: BASE_URL,
|
||||
viewerUserId: viewer.userId,
|
||||
workspaceId: fixture.workspaceId,
|
||||
primaryId: fixture.primaryId,
|
||||
siblingId: fixture.siblingId,
|
||||
renamedTitle: fixture.renamedTitle,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
} finally {
|
||||
if (fixture) {
|
||||
try {
|
||||
await cleanupDocuments(context.request, fixture.createdIds);
|
||||
} catch (cleanupError) {
|
||||
if (!caughtError) {
|
||||
caughtError = cleanupError;
|
||||
} else {
|
||||
console.error(
|
||||
`清理临时页面失败:${
|
||||
cleanupError instanceof Error
|
||||
? cleanupError.stack || cleanupError.message
|
||||
: String(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);
|
||||
});
|
||||
Reference in New Issue
Block a user