test: organize current smoke baseline
This commit is contained in:
+2
-2
@@ -42,8 +42,8 @@ function parseArgs(argv) {
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
console.log(
|
||||
[
|
||||
"用法:node scripts/export-convex-workspace-to-local.js --fixture <fixture.json> --out <dir> [--dry-run] [--manifest <file>] [--conflict-report <file>]",
|
||||
"回滚:node scripts/export-convex-workspace-to-local.js --rollback <manifest.json>",
|
||||
"用法:node recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js --fixture <fixture.json> --out <dir> [--dry-run] [--manifest <file>] [--conflict-report <file>]",
|
||||
"回滚:node recycle/scripts/retired-convex-export-smokes-20260526/export-convex-workspace-to-local.js --rollback <manifest.json>",
|
||||
].join("\n"),
|
||||
);
|
||||
process.exit(0);
|
||||
+2
-2
@@ -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",
|
||||
+2
-2
@@ -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);
|
||||
@@ -9,6 +9,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
|
||||
currentRootUri,
|
||||
currentWorkspaceSourcePayload,
|
||||
fileTreeIconKindForFileName,
|
||||
healLegacyOfficeAttachmentParagraphs,
|
||||
inferCodeAttachmentLanguage,
|
||||
inferOnlyOfficeFileType,
|
||||
isCodeAttachmentFileName,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn AuthPage() -> impl IntoView {
|
||||
<h1 id="mnote-auth-title">"账号登录"</h1>
|
||||
<p>"登录后进入你的工作区"</p>
|
||||
</div>
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="convex-password">
|
||||
<form class="mnote-auth-form" method="post" action="/api/auth" data-auth-mode="sqlite-session">
|
||||
<input type="hidden" name="action" value="auth:signIn" />
|
||||
<input type="hidden" name="provider" value="password" />
|
||||
<input type="hidden" name="flow" value="signIn" data-auth-flow />
|
||||
@@ -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');
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user