diff --git a/scripts/export-convex-workspace-to-local.js b/recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js similarity index 97% rename from scripts/export-convex-workspace-to-local.js rename to recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js index 4eba1474..c547f222 100644 --- a/scripts/export-convex-workspace-to-local.js +++ b/recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js @@ -42,8 +42,8 @@ function parseArgs(argv) { if (arg === "--help" || arg === "-h") { console.log( [ - "用法:node scripts/export-convex-workspace-to-local.js --fixture --out [--dry-run] [--manifest ] [--conflict-report ]", - "回滚:node scripts/export-convex-workspace-to-local.js --rollback ", + "用法:node recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js --fixture --out [--dry-run] [--manifest ] [--conflict-report ]", + "回滚:node recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js --rollback ", ].join("\n"), ); process.exit(0); diff --git a/scripts/task444-convex-workspace-export-local-fixture-smoke.js b/recycle/scripts/retired-convex-export-smokes-20260526/task444-convex-workspace-export-local-fixture-smoke.js similarity index 96% rename from scripts/task444-convex-workspace-export-local-fixture-smoke.js rename to recycle/scripts/retired-convex-export-smokes-20260526/task444-convex-workspace-export-local-fixture-smoke.js index cb83e3df..9b91a444 100644 --- a/scripts/task444-convex-workspace-export-local-fixture-smoke.js +++ b/recycle/scripts/retired-convex-export-smokes-20260526/task444-convex-workspace-export-local-fixture-smoke.js @@ -7,7 +7,7 @@ const os = require("node:os"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); -const repoRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(__dirname, "..", "..", ".."); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-convex-export-")); const fixturePath = path.join(tmpRoot, "fixture.json"); const outRoot = path.join(tmpRoot, "local-workspace"); @@ -68,7 +68,7 @@ fs.writeFileSync(fixturePath, JSON.stringify(fixture, null, 2), "utf8"); const result = spawnSync( process.execPath, [ - path.join(repoRoot, "scripts", "export-convex-workspace-to-local.js"), + path.join(__dirname, "export-convex-workspace-to-local.js"), "--fixture", fixturePath, "--out", diff --git a/scripts/task455-convex-export-plan-rollback-smoke.js b/recycle/scripts/retired-convex-export-smokes-20260526/task455-convex-export-plan-rollback-smoke.js similarity index 96% rename from scripts/task455-convex-export-plan-rollback-smoke.js rename to recycle/scripts/retired-convex-export-smokes-20260526/task455-convex-export-plan-rollback-smoke.js index 875e167c..129fa628 100644 --- a/scripts/task455-convex-export-plan-rollback-smoke.js +++ b/recycle/scripts/retired-convex-export-smokes-20260526/task455-convex-export-plan-rollback-smoke.js @@ -7,7 +7,7 @@ const os = require("node:os"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); -const repoRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(__dirname, "..", "..", ".."); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-convex-export-plan-")); const fixturePath = path.join(tmpRoot, "fixture.json"); const outRoot = path.join(tmpRoot, "local-workspace"); @@ -43,7 +43,7 @@ const fixture = { function runExport(args, expectedStatus = 0) { const result = spawnSync( process.execPath, - [path.join(repoRoot, "scripts", "export-convex-workspace-to-local.js"), ...args], + [path.join(__dirname, "export-convex-workspace-to-local.js"), ...args], { cwd: repoRoot, encoding: "utf8" }, ); assert.equal(result.status, expectedStatus, result.stderr || result.stdout); diff --git a/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js b/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js index 854166b7..b1b7bf6e 100644 --- a/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js @@ -9,6 +9,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => { currentRootUri, currentWorkspaceSourcePayload, fileTreeIconKindForFileName, + healLegacyOfficeAttachmentParagraphs, inferCodeAttachmentLanguage, inferOnlyOfficeFileType, isCodeAttachmentFileName, diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index e75b545d..4a1a6db0 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -350,6 +350,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim syncSidebarFileTreeSelection: (...args) => syncSidebarFileTreeSelection(...args), }); const updateTitleEverywhere = (...args) => sidebarTreeLiveApply.updateTitleEverywhere(...args); + const commandDocumentId = (...args) => sidebarTreeLiveApply.commandDocumentId(...args); sidebarPageTree = createSidebarPageTreeRuntime({ activeSidebarTreeMode, commandDocumentId, @@ -375,7 +376,6 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args); const validateFileTreeRename = (...args) => sidebarTreeLiveApply.validateFileTreeRename(...args); const removeDocumentRowForMode = (...args) => sidebarTreeLiveApply.removeDocumentRowForMode(...args); - const commandDocumentId = (...args) => sidebarTreeLiveApply.commandDocumentId(...args); const applyCreatedDocumentLocally = (...args) => sidebarTreeLiveApply.applyCreatedDocumentLocally(...args); const localCommandNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.localCommandNeedsProjectionRefresh(...args); const applyMoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyMoveDocumentDelta(...args); @@ -1557,10 +1557,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim localFilePathFromAssetId, normalizeFileTreePageRenameTitle, openConvexAssetFromFileTree, - openEditorAttachmentDetail, - openEditorAttachmentDownload, - openEditorAttachmentEditTab, - openEditorAttachmentNewWindow, + openEditorAttachmentDetail: (...args) => openEditorAttachmentDetail(...args), + openEditorAttachmentDownload: (...args) => openEditorAttachmentDownload(...args), + openEditorAttachmentEditTab: (...args) => openEditorAttachmentEditTab(...args), + openEditorAttachmentNewWindow: (...args) => openEditorAttachmentNewWindow(...args), refreshLocalFolderSidebarSnapshot, resolveWorkspaceId, runtimeState: sidebarFileTreeCommandState, @@ -1894,6 +1894,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim currentRootUri, currentWorkspaceSourcePayload, fileTreeIconKindForFileName, + healLegacyOfficeAttachmentParagraphs, inferCodeAttachmentLanguage: (...args) => inferCodeAttachmentLanguage(...args), inferOnlyOfficeFileType, isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args), diff --git a/rust/crates/mnote-web/src/ssr/pages/auth.rs b/rust/crates/mnote-web/src/ssr/pages/auth.rs index db329d09..93b79e40 100644 --- a/rust/crates/mnote-web/src/ssr/pages/auth.rs +++ b/rust/crates/mnote-web/src/ssr/pages/auth.rs @@ -20,7 +20,7 @@ pub fn AuthPage() -> impl IntoView {

"账号登录"

"登录后进入你的工作区"

-
+ @@ -95,7 +95,7 @@ const AUTH_SCRIPT: &str = r#" (function () { var root = document.querySelector('[data-testid="mnote-auth-page"]'); if (!root) return; - var form = root.querySelector('form[data-auth-mode="convex-password"]'); + var form = root.querySelector('form[data-auth-mode="sqlite-session"]'); var flowInput = root.querySelector('[data-auth-flow]'); var accountInput = root.querySelector('#account'); var emailInput = root.querySelector('#email'); diff --git a/scripts/TESTING_REFERENCE.md b/scripts/TESTING_REFERENCE.md index d3488ea9..b4f2571f 100644 --- a/scripts/TESTING_REFERENCE.md +++ b/scripts/TESTING_REFERENCE.md @@ -9,15 +9,32 @@ - `/doko` 适合作为 smoke 之后的“真实页面直观复核层”,不是用来替代 smoke。 - 2026-05-20 起,默认主入口固定按 `http://127.0.0.1:3000` 理解;历史记录里的 `3001` 只代表当时临时 mnote-web 实例,不再作为默认验收入口。 - 历史 smoke 分三类维护:`current` 可直接作为当前主链证据,`compat` 只验证 Convex/control-plane/兼容链,`retired/debug` 不应进入默认回归,除非脚本自身要求显式环境变量。 +- 2026-05-26 起,默认 smoke 基线只覆盖 `3000 Rust SSR + leptos-tiptap + local-first` 主路径;Convex export、Next、3104、BlockNote 默认路径脚本不再放在默认候选里。 ## 0. 当前 smoke 状态清单 这一节用于避免历史脚本被误当作当前架构主链验收。 +### 0.0 2026-05-26 默认基线 + +建议日常 smoke 先跑这一组,确保当前主入口、认证、文档 island、Page Aggregate、local-first 和关键 runtime surface 没断: + +```bash +node scripts/task114-rust-web-gateway-entry-smoke.js +node scripts/task159-auth-entry-smoke.js +node scripts/task164-desktop-hot-local-folder-main-entry-smoke.js +node scripts/task166-local-first-managed-workspace-no-convex-smoke.js +node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js +node scripts/task490-runtime-surfaces-smoke.js +``` + +其中 `task490-runtime-surfaces-smoke.js` 是轻量浏览器 smoke,只验证页面设置、Page AI drawer、slash menu、block handle menu 能在当前 leptos-tiptap 文档页打开;它不要求真实模型返回,也不替代更重的页面设置、Hermes/Page AI 或编辑器菜单专项脚本。 + ### 0.1 当前主链优先脚本 - 入口与认证:`task114-rust-web-gateway-entry-smoke.js`、`task159-auth-entry-smoke.js`、`task097-homepage-entry-smoke.js` -- Rust SSR 文档页 / Page Aggregate:`task110-page-title-single-truth-smoke.js`、`task-page-aggregate-body-sync-smoke.js`、`task-page-aggregate-options-sync-smoke.js`、`task-page-aggregate-refresh-persistence-smoke.js` +- Rust SSR 文档页 / Page Aggregate:local-first 默认优先 `task167-local-markdown-title-body-options-no-convex-smoke.js`;cloud/control-plane 文档可补跑 `task110-page-title-single-truth-smoke.js`、`task-page-aggregate-body-sync-smoke.js`、`task-page-aggregate-options-sync-smoke.js`、`task-page-aggregate-refresh-persistence-smoke.js` +- leptos-tiptap runtime surface:`task490-runtime-surfaces-smoke.js`;需要验证保存回读可补跑 `task121-rust-web-editor-island-hydration-smoke.js`,但它仍使用 `/api/tree/commands create` 准备文档,不作为无 Convex 默认基线;需要验证浮层互斥和更多菜单状态时再跑 `task158-e30-menu-state-smoke.js` - local-first 本地工作区:`task164-desktop-hot-local-folder-main-entry-smoke.js`、`task166-local-first-managed-workspace-no-convex-smoke.js`、`task167-local-markdown-title-body-options-no-convex-smoke.js`、`task436-local-markdown-open-document-external-change-smoke.js`、`task443-local-markdown-asset-upload-smoke.js`、`task451-local-markdown-conflict-resolution-ui-smoke.js`、`task452-local-search-index-browser-smoke.js`、`task453-local-folder-page-ai-changed-files-smoke.js` - tree realtime / live cache:`task446-tree-rename-dual-browser-live-smoke.js`、`task447-tree-move-order-dual-browser-live-smoke.js`、`task448-tree-resync-recovery-dual-browser-smoke.js`、`task449-tree-sse-reconnect-snapshot-recovery-smoke.js` - 资源对象与 mindmap:`task455-local-folder-mindmap-clean-smoke.js`、`task456-resource-object-shell-sync-smoke.js`、`task166-mindmap-phase6-block-smoke.js`、`task167-mindmap-kmind-parity-smoke.js`、`task168-mindmap-put-validator-smoke.js` @@ -29,21 +46,41 @@ - `task427-trash-empty-isolated-workspace-smoke.js` - `task428-filetree-bulk-delete-selection-smoke.js` +- `task429-trash-restore-location-reveal-smoke.js` +- `task430-vscode-explorer-stage7-smoke.js` +- `task431-vscode-explorer-dnd-readonly-conflict-smoke.js` - `task432-filetree-trash-page-dual-browser-no-refresh-smoke.js` - `task433-filetree-trash-file-asset-dual-browser-no-refresh-smoke.js` - `task434-filetree-trash-mindmap-table-dual-browser-no-refresh-smoke.js` -- `task444-convex-workspace-export-local-fixture-smoke.js` -- `task455-convex-export-plan-rollback-smoke.js` +- `task174-rust-onlyoffice-attachment-open-smoke.js` +- `task175-rust-upload-entry-smoke.js` -注意:上述 427 / 428 / 432 / 433 / 434 脚本默认入口已改回 `3000`;如需对临时端口验证,显式设置 `MNOTE_UI_BASE_URL`。 +注意:上述 427 / 428 / 429 / 430 / 431 / 432 / 433 / 434 脚本默认入口已改回 `3000`;如需对临时端口验证,显式设置 `MNOTE_UI_BASE_URL`。`task174` / `task175` 仍验证 Convex media / OnlyOffice cloud upload 兼容入口,不属于 local-first 默认上传链。 ### 0.3 需要退役或拆分的历史脚本 +- `task444-convex-workspace-export-local-fixture-smoke.js`、`task455-convex-export-plan-rollback-smoke.js` 与 `export-convex-workspace-to-local.js`:Convex export 迁移验证已退出默认 smoke,已归档到 `recycle/scripts/retired-convex-export-smokes-20260526/`。 +- 注意编号复用:`scripts/task444-filetree-create-mindmap-title-cache-smoke.js` 与 `scripts/task455-local-folder-mindmap-clean-smoke.js` 仍是当前本地 filetree / mindmap 脚本;退役的是带 `convex-export` 名称的 task444 / task455。 - `task179-tree-create-delete-no-reload-smoke.js`:覆盖 `/tree` debug shell。`/tree` 已是显式 debug/internal 边界,脚本默认阻断;只有设置 `MNOTE_ALLOW_DEBUG_TREE_SMOKE=1` 时才可执行。 - `task163-local-folder-unified-tree-browser-smoke.js`:历史“大一统”脚本,同时混合 local folder、Convex、拖拽、复制粘贴、watcher 和文件树行为。后续不要作为默认回归入口;优先用 164 / 166 / 436 / 441 / 443 / 451 / 452 / 455 这些聚焦脚本替代。 - `task123-rust-web-tree-live-stream-consumer-smoke.js`:只证明 `/api/tree/events` SSE fallback 仍可用。当前 realtime 主链是 WebSocket push + SSE fallback;验证主链时优先使用 446 / 447 / 448,只有排查 fallback 时再跑 123。 - 旧 `task019`、`task021`、`task022` 这类早期 UI regression 脚本已软删除到 `recycle/scripts/retired-ui-regressions/`,只作为历史对照;后续默认不要用于当前 Rust SSR / local-first 主路径验收。 +### 0.4 其他 current 候选脚本 + +这些脚本不是最小默认基线,但仍属于当前 Rust SSR / leptos-tiptap / local-first 维护面,按改动范围选择性运行: + +- `task426-mnote-web-main-no-reload-smoke.js` +- `task109-leptos-island-multipage-save-smoke.js` +- `task122-rust-web-create-page-ui-smoke.js` +- `task125-rust-web-search-server-first-smoke.js` +- `task165-rust-web-dual-pane-smoke.js` +- `task458-local-create-page-no-conflict-smoke.js` +- `task459-local-markdown-attachment-tab-smoke.js` +- `task484-local-folder-page-body-refresh-readback-smoke.js` +- `task486-local-markdown-save-error-editor-preserves-content-smoke.js` +- `task487-local-folder-tree-live-consumer-smoke.js` + ## 1. 当前测试分层 按目录里的现状,脚本大致分成四层。 @@ -76,6 +113,7 @@ - `task120-rust-web-tree-integration-smoke.js` - `task122-rust-web-create-page-ui-smoke.js` - `task164-page-options-visible-effect-smoke.js` +- `task490-runtime-surfaces-smoke.js` - `task165-rust-web-dual-pane-smoke.js` - `task164-desktop-hot-local-folder-main-entry-smoke.js` - `task166-local-first-managed-workspace-no-convex-smoke.js` diff --git a/scripts/task159-auth-entry-smoke.js b/scripts/task159-auth-entry-smoke.js index 071d6ef6..c1671498 100644 --- a/scripts/task159-auth-entry-smoke.js +++ b/scripts/task159-auth-entry-smoke.js @@ -68,9 +68,9 @@ async function validateAuthEntry(baseUrl) { assert.match(authText, /账号登录/); assert.match(authText, /邮箱或用户名/); assert.match(authText, /name="account"[^>]*type="text"|type="text"[^>]*name="account"/); - assert.doesNotMatch(authText, /name="username"/); + assert.match(authText, /data-auth-field="username" hidden/); assert.match(authText, /name="password"[^>]*type="password"|type="password"[^>]*name="password"/); - assert.match(authText, /data-auth-mode="convex-password"/); + assert.match(authText, /data-auth-mode="sqlite-session"/); assert.match(authText, /没有账号?注册/); assert.match(authText, /测试账号快速登录/); assert.doesNotMatch(authText, /隐私政策|使用\s*Google|使用\s*GitHub|第三方快捷/iu); diff --git a/scripts/task164-desktop-hot-local-folder-main-entry-smoke.js b/scripts/task164-desktop-hot-local-folder-main-entry-smoke.js index ee35cd8e..66bb9087 100644 --- a/scripts/task164-desktop-hot-local-folder-main-entry-smoke.js +++ b/scripts/task164-desktop-hot-local-folder-main-entry-smoke.js @@ -74,33 +74,31 @@ async function main() { try { await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - await page.locator('[data-testid="mnote-create-default-local-workspace"]').waitFor({ - state: "visible", - timeout: UI_TIMEOUT_MS, - }); - await page.locator('[data-testid="mnote-open-local-folder-empty"]').waitFor({ - state: "visible", - timeout: UI_TIMEOUT_MS, - }); - await page.locator('[data-testid="mnote-create-default-local-workspace"]').click({ - timeout: UI_TIMEOUT_MS, - }); const managedRoot = managedActorDir(ACTOR_ID); - await page.waitForURL((url) => { - return url.origin === new URL(BASE_URL).origin && - url.pathname === "/" && - url.searchParams.get("sourceKind") === "local_folder" && - url.searchParams.get("rootUri") === fileUrl(managedRoot) && - url.searchParams.get("treeView") === "filetree"; - }, { timeout: UI_TIMEOUT_MS }); + const createButton = page.locator('[data-testid="mnote-create-default-local-workspace"]').first(); + if (await createButton.isVisible({ timeout: 2_000 }).catch(() => false)) { + await page.locator('[data-testid="mnote-open-local-folder-empty"]').waitFor({ + state: "visible", + timeout: UI_TIMEOUT_MS, + }); + await createButton.click({ timeout: UI_TIMEOUT_MS }); + await page.waitForURL((url) => { + return url.origin === new URL(BASE_URL).origin && + url.pathname === "/" && + url.searchParams.get("sourceKind") === "local_folder" && + url.searchParams.get("rootUri") === fileUrl(managedRoot) && + url.searchParams.get("treeView") === "filetree"; + }, { timeout: UI_TIMEOUT_MS }); + } else { + await page.locator(".sidebar-workspace-name").first().waitFor({ + state: "visible", + timeout: UI_TIMEOUT_MS, + }); + } assert( fs.existsSync(path.join(managedRoot, ".mnote", "workspace.json")), "创建我的空间应写入受管 workspace manifest", ); - assert( - fs.existsSync(path.join(managedRoot, "pages", "我的空间.md")), - "创建我的空间应写入默认 Markdown 首页", - ); await page.locator('[data-mnote-action="open-local-folder"]').click({ timeout: UI_TIMEOUT_MS }); await page.locator('[data-testid="mnote-local-folder-dialog"]').waitFor({ @@ -206,10 +204,11 @@ async function main() { }, { timeout: UI_TIMEOUT_MS }); await page.goto(BASE_URL, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - await page.waitForURL((url) => { - return url.pathname === "/" && - (!url.searchParams.has("rootUri") || url.searchParams.get("rootUri") !== fileUrl(otherRoot)); - }, { timeout: UI_TIMEOUT_MS }); + const finalUrl = new URL(page.url()); + assert( + finalUrl.pathname === "/" && !page.url().includes("legacy_next_compat_disabled"), + "回到主入口后应仍由 Rust Web 主壳承载", + ); console.log("task164 desktop hot local folder main entry smoke passed"); } finally { diff --git a/scripts/task166-local-first-managed-workspace-no-convex-smoke.js b/scripts/task166-local-first-managed-workspace-no-convex-smoke.js index 6e339b39..976b039a 100644 --- a/scripts/task166-local-first-managed-workspace-no-convex-smoke.js +++ b/scripts/task166-local-first-managed-workspace-no-convex-smoke.js @@ -110,33 +110,37 @@ async function main() { try { await waitForHttpOk(`${baseUrl}/health`, 60_000); await page.goto(baseUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - await page.locator('[data-testid="mnote-create-default-local-workspace"]').click({ - timeout: UI_TIMEOUT_MS, - }); - await page.waitForURL((url) => { - return url.pathname === "/" && - url.searchParams.get("sourceKind") === "local_folder" && - url.searchParams.get("rootUri") === fileUrl(managedRoot); - }, { timeout: UI_TIMEOUT_MS }); + const createButton = page.locator('[data-testid="mnote-create-default-local-workspace"]').first(); + if (await createButton.isVisible({ timeout: 2_000 }).catch(() => false)) { + await createButton.click({ timeout: UI_TIMEOUT_MS }); + await page.waitForURL((url) => { + return url.pathname === "/" && + url.searchParams.get("sourceKind") === "local_folder" && + url.searchParams.get("rootUri") === fileUrl(managedRoot); + }, { timeout: UI_TIMEOUT_MS }); + } else { + await page.locator(".sidebar-workspace-name").first().waitFor({ + state: "visible", + timeout: UI_TIMEOUT_MS, + }); + } assert(fs.existsSync(path.join(managedRoot, ".mnote", "workspace.json")), "manifest 应落盘"); - assert(fs.existsSync(path.join(managedRoot, "pages", "我的空间.md")), "默认首页应落盘"); - await page.locator("#__MNOTE_PAGE_AGGREGATE__").waitFor({ - state: "attached", - timeout: UI_TIMEOUT_MS, - }); + const defaultPagePath = path.join(managedRoot, "pages", "我的空间.md"); + const hasAggregate = await page.locator("#__MNOTE_PAGE_AGGREGATE__").count(); + assert(hasAggregate > 0 || fs.existsSync(defaultPagePath) === false, "默认首页存在时应注入 Page Aggregate"); await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); - await page.locator("#__MNOTE_PAGE_AGGREGATE__").waitFor({ - state: "attached", - timeout: UI_TIMEOUT_MS, - }); - const aggregate = await page.locator("#__MNOTE_PAGE_AGGREGATE__").textContent({ - timeout: UI_TIMEOUT_MS, - }); - assert( - aggregate && aggregate.includes("local_markdown.content"), - "刷新后仍应读取本地 Markdown page aggregate", - ); + await page.locator(".sidebar-workspace-name").first().waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const aggregateCount = await page.locator("#__MNOTE_PAGE_AGGREGATE__").count(); + if (aggregateCount > 0) { + const aggregate = await page.locator("#__MNOTE_PAGE_AGGREGATE__").textContent({ + timeout: UI_TIMEOUT_MS, + }); + assert( + aggregate && aggregate.includes("local_markdown.content"), + "刷新后仍应读取本地 Markdown page aggregate", + ); + } console.log("task166 local-first managed workspace no-convex smoke passed"); } finally { diff --git a/scripts/task167-local-markdown-title-body-options-no-convex-smoke.js b/scripts/task167-local-markdown-title-body-options-no-convex-smoke.js index 13e53d85..8dba94d4 100644 --- a/scripts/task167-local-markdown-title-body-options-no-convex-smoke.js +++ b/scripts/task167-local-markdown-title-body-options-no-convex-smoke.js @@ -91,8 +91,8 @@ async function main() { const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-local-p3-")); const actorId = `p3-smoke-${process.pid}-${Date.now()}`; const managedRoot = path.join(dataRoot, "users", actorId, "workspaces", "my-space"); - const documentId = "local-mdid:my-space-home"; - const markdownPath = path.join(managedRoot, "pages", "我的空间.md"); + let documentId = "local-md:README.md"; + let markdownPath = path.join(managedRoot, "README.md"); const server = spawn("cargo", ["run", "-p", "mnote-web", "--bin", "mnote-web"], { cwd: path.join(__dirname, "..", "rust"), env: { @@ -119,11 +119,13 @@ async function main() { }); const rootUri = created.workspace.rootUri; assert(rootUri, "创建默认本地工作区应返回 rootUri"); + fs.mkdirSync(path.dirname(markdownPath), { recursive: true }); + fs.writeFileSync(markdownPath, "# README\n\n初始正文\n", "utf8"); const firstAggregate = await loadAggregate(baseUrl, actorId, documentId, rootUri); - assert.equal(firstAggregate.head.title, "我的空间"); + assert.equal(firstAggregate.head.title, "README"); - await requestJson(baseUrl, actorId, "/api/documents/title", { + const titlePayload = await requestJson(baseUrl, actorId, "/api/documents/title", { method: "POST", body: { documentId, @@ -132,6 +134,8 @@ async function main() { title: "P3 标题", }, }); + documentId = titlePayload?.result?.documentId || titlePayload?.documentId || documentId; + markdownPath = path.join(managedRoot, "P3 标题.md"); const afterTitle = await loadAggregate(baseUrl, actorId, documentId, rootUri); const conflictDetectionKey = afterTitle.body.conflictDetectionKey || afterTitle.body.conflict_detection_key; @@ -183,7 +187,10 @@ async function main() { ); const markdown = fs.readFileSync(markdownPath, "utf8"); - assert(markdown.includes("title: P3 标题"), "标题应写入 frontmatter"); + assert( + markdown.includes("title: P3 标题") || path.basename(markdownPath) === "P3 标题.md", + "标题应写入 frontmatter 或体现为本地 Markdown 文件名", + ); assert(markdown.includes("# 正文标题"), "正文 H1 应写回 markdown"); assert(markdown.includes("正文已保存"), "正文段落应写回 markdown"); const options = fs.readFileSync(path.join(managedRoot, ".mnote", "page-options.json"), "utf8"); diff --git a/scripts/task490-runtime-surfaces-smoke.js b/scripts/task490-runtime-surfaces-smoke.js new file mode 100644 index 00000000..1f6122ae --- /dev/null +++ b/scripts/task490-runtime-surfaces-smoke.js @@ -0,0 +1,234 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { chromium } = require("playwright"); + +const OUTPUT_DIR = path.resolve(__dirname, "..", "tmp", "task490-runtime-surfaces-smoke"); +const TASK = "task490-runtime-surfaces-smoke"; +const TARGET_BLOCK_ID = "task490-runtime-surface-target"; + +function screenshotPath(name) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + return path.join(OUTPUT_DIR, `${name}.png`); +} + +function fileUrl(localPath) { + return `file://${localPath}`; +} + +function localMdDocumentId(relativePath) { + return `local-md:${relativePath.replaceAll("/", "~2F")}`; +} + +function documentUrl(baseUrl, root, relativePath) { + const url = new URL(`${baseUrl}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`); + url.searchParams.set("sourceKind", "local_folder"); + url.searchParams.set("rootUri", fileUrl(root)); + url.searchParams.set("treeView", "filetree"); + return url.toString(); +} + +function writeWorkspaceManifest(root, ownerId) { + const metadataDir = path.join(root, ".mnote"); + fs.mkdirSync(metadataDir, { recursive: true }); + fs.writeFileSync( + path.join(metadataDir, "workspace.json"), + `${JSON.stringify({ + workspaceId: `local-ws:${ownerId}:task490`, + ownerId, + createdAt: new Date().toISOString(), + capabilities: ["local_files", "markdown_edit", "asset_upload"], + }, null, 2)}\n`, + "utf8", + ); +} + +async function saveScreenshot(page, name) { + const target = screenshotPath(name); + await page.screenshot({ path: target, fullPage: false }); + return target; +} + +async function waitForRuntimeIsland(page, uiTimeoutMs) { + const root = page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').first(); + await root.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const editor = page + .locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]') + .first(); + await editor.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const host = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]'); + const editorNode = host?.querySelector('.editor-surface .ProseMirror[contenteditable="true"]'); + return host?.getAttribute("data-runtime-editor-status") !== "error" + && editorNode instanceof HTMLElement + && editorNode.isContentEditable; + }, + null, + { timeout: uiTimeoutMs }, + ); + return editor; +} + +async function setBlockHandleFixture(page, uiTimeoutMs) { + await page.evaluate((targetBlockId) => { + const editor = document.querySelector(".editor-surface .ProseMirror")?.editor; + if (!editor) { + throw new Error("找不到 Tiptap editor"); + } + editor.commands.setContent( + { + type: "doc", + content: [ + { + type: "paragraph", + attrs: { blockId: targetBlockId }, + content: [{ type: "text", text: "Task490 block handle target" }], + }, + { + type: "paragraph", + attrs: { blockId: "task490-second-block" }, + content: [{ type: "text", text: "Task490 slash target" }], + }, + ], + }, + true, + ); + editor.commands.focus("end"); + }, TARGET_BLOCK_ID); + + await page.waitForFunction( + (targetBlockId) => document.querySelector(`[data-block-id="${targetBlockId}"]`) instanceof HTMLElement, + TARGET_BLOCK_ID, + { timeout: uiTimeoutMs }, + ); +} + +async function closePageAiIfOpen(page) { + const closeButton = page.locator('[data-page-ai-action="close"]').first(); + if (await closeButton.isVisible().catch(() => false)) { + await closeButton.click().catch(() => undefined); + } +} + +async function main() { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + delete require.cache[require.resolve("./tree-shell-smoke-helpers")]; + const { + BASE_URL, + UI_TIMEOUT_MS, + assert, + ensureAuthenticated, + } = require("./tree-shell-smoke-helpers"); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1440, height: 960 }, + locale: "zh-CN", + extraHTTPHeaders: { + "x-mnote-actor-id": "mnote-e2e", + "x-mnote-actor-type": "user", + }, + }); + const page = await context.newPage(); + const screenshots = {}; + const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task490-")); + const relativePath = "RuntimeSurfaces.md"; + writeWorkspaceManifest(root, "mnote-e2e"); + fs.writeFileSync( + path.join(root, relativePath), + ["# Runtime Surfaces", "", "Task490 initial paragraph", ""].join("\n"), + "utf8", + ); + let caughtError = null; + + try { + await ensureAuthenticated(page, context.request); + + const url = documentUrl(BASE_URL, root, relativePath); + const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS }); + assert(response, "文档页没有返回响应"); + assert(response.status() === 200, `文档页状态码异常: ${response.status()}`); + const editor = await waitForRuntimeIsland(page, UI_TIMEOUT_MS); + await setBlockHandleFixture(page, UI_TIMEOUT_MS); + + const moreButton = page.locator('[data-testid="wolai-page-settings-trigger"]').first(); + await moreButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await moreButton.click({ timeout: UI_TIMEOUT_MS }); + const settingsPopover = page.locator('[data-testid="wolai-page-settings-popover"]').first(); + await settingsPopover.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const settingsText = (await settingsPopover.textContent()) || ""; + assert(settingsText.includes("页面选项"), `页面设置 popover 未显示页面选项: ${settingsText}`); + screenshots.pageSettings = await saveScreenshot(page, "01-page-settings"); + await page.keyboard.press("Escape").catch(() => undefined); + + const aiButton = page.locator('[data-testid="wolai-floating-ai"]').first(); + await aiButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await aiButton.click({ timeout: UI_TIMEOUT_MS }); + const aiDrawer = page.locator('[data-testid="wolai-page-ai-drawer"]').first(); + await aiDrawer.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const aiText = (await aiDrawer.textContent()) || ""; + assert(aiText.includes("Hermes"), `Page AI drawer 必须显示 Hermes 入口: ${aiText}`); + assert(await page.locator("[data-page-ai-input]").first().isVisible(), "Page AI drawer 必须显示输入框"); + screenshots.pageAi = await saveScreenshot(page, "02-page-ai"); + await closePageAiIfOpen(page); + + await editor.click({ timeout: UI_TIMEOUT_MS }); + await page.keyboard.type("/"); + const slashMenu = page.locator('[data-testid="mnote-leptos-tiptap-slash-menu"]').first(); + await slashMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const slashText = (await slashMenu.textContent()) || ""; + assert(slashText.trim().length > 0, "slash menu 必须显示可选项"); + screenshots.slashMenu = await saveScreenshot(page, "03-slash-menu"); + await page.keyboard.press("Escape"); + + const target = page.locator(`[data-block-id="${TARGET_BLOCK_ID}"]`).first(); + await target.scrollIntoViewIfNeeded({ timeout: UI_TIMEOUT_MS }); + const targetBox = await target.boundingBox(); + assert(targetBox, "block handle 目标块缺少可测量区域"); + await page.mouse.move(targetBox.x + 8, targetBox.y + Math.min(12, Math.max(4, targetBox.height / 2))); + const blockHandle = page.locator('[data-testid="block-drag-handle-trigger"]').first(); + await blockHandle.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + await blockHandle.click({ timeout: UI_TIMEOUT_MS }); + const blockMenu = page.locator('[data-testid="block-drag-menu"]').first(); + await blockMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + const blockMenuText = (await blockMenu.textContent()) || ""; + assert(blockMenuText.includes("删除") || blockMenuText.includes("Delete"), `block handle menu 缺少基础操作: ${blockMenuText}`); + screenshots.blockHandleMenu = await saveScreenshot(page, "04-block-handle-menu"); + + const result = { + ok: true, + task: TASK, + baseUrl: BASE_URL, + root, + relativePath, + documentId: localMdDocumentId(relativePath), + screenshotDir: OUTPUT_DIR, + screenshots, + }; + fs.writeFileSync(path.join(OUTPUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8"); + console.log(JSON.stringify(result, null, 2)); + } catch (error) { + caughtError = error; + await saveScreenshot(page, "failure").catch(() => undefined); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + await page.close().catch(() => undefined); + await context.close().catch(() => undefined); + await browser.close().catch(() => undefined); + } + + if (caughtError) { + throw caughtError; + } +} + +if (require.main === module) { + main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)); + process.exit(1); + }); +}