feat: 收口 Rust Web 3000 主链
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
# mnote 测试参考
|
||||
|
||||
这份文档面向 `/mnt/Data1T/mnote/scripts` 目录下的现有测试脚本,目标不是重新设计测试体系,而是把当前已经在用的 smoke、回归脚本、截图取证和人工复核方式整理成一套可执行参考。
|
||||
|
||||
当前结论先说在前面:
|
||||
|
||||
- `scripts/` 里的主力仍然是 `Playwright smoke + Node 断言`。
|
||||
- 对 `mnote` 来说,最有价值的不是把所有测试都换成“浏览器插件式控制”,而是把已有 smoke 的结果变得更可看、更易复核。
|
||||
- `/doko` 适合作为 smoke 之后的“真实页面直观复核层”,不是用来替代 smoke。
|
||||
|
||||
## 1. 当前测试分层
|
||||
|
||||
按目录里的现状,脚本大致分成四层。
|
||||
|
||||
### 1.1 入口与网关层
|
||||
|
||||
这一层主要验证路由、网关、owner、认证入口、基础响应是否正确,特点是快、稳定、适合作为最早执行的守门测试。
|
||||
|
||||
代表脚本:
|
||||
|
||||
- `task097-homepage-entry-smoke.js`
|
||||
- `task114-rust-web-gateway-entry-smoke.js`
|
||||
- `task117-next-retirement-guard.js`
|
||||
- `task159-auth-entry-smoke.js`
|
||||
|
||||
适用场景:
|
||||
|
||||
- 刚切流 Rust Web
|
||||
- 改了入口路由、鉴权、兼容代理
|
||||
- 需要先判断“服务有没有活着、入口是不是对的”
|
||||
|
||||
### 1.2 页面交互 smoke 层
|
||||
|
||||
这一层是当前最核心的 UI 验证方式:启动页面、登录测试账号、进入真实文档页或工作区,然后通过 Playwright 操作页面并做断言。
|
||||
|
||||
代表脚本:
|
||||
|
||||
- `task110-page-title-single-truth-smoke.js`
|
||||
- `task112-tree-rust-family-regression-smoke.js`
|
||||
- `task120-rust-web-tree-integration-smoke.js`
|
||||
- `task122-rust-web-create-page-ui-smoke.js`
|
||||
- `task163-local-folder-unified-tree-browser-smoke.js`
|
||||
- `task164-page-options-visible-effect-smoke.js`
|
||||
- `task165-rust-web-dual-pane-smoke.js`
|
||||
|
||||
这一层已经覆盖了当前主线里最重要的部分:
|
||||
|
||||
- tree / sidebar / create page
|
||||
- document shell / island hydration
|
||||
- 页面设置、双栏、AI、local folder
|
||||
- Rust Web 主路径
|
||||
|
||||
### 1.3 Wolai 对标与视觉取证层
|
||||
|
||||
这一层不是单纯判断“是否通过”,而是要保留截图、矩阵、对比结论,方便和 Wolai 体验做真实核对。
|
||||
|
||||
代表脚本:
|
||||
|
||||
- `task119-rust-web-wolai-visual-regression-smoke.js`
|
||||
- `task129-wolai-aline-baseline-smoke.js`
|
||||
- `task130` 到 `task137` 一系列 `wolai-aline-*`
|
||||
- `task160-wolai-page-settings-shell-smoke.js`
|
||||
- `task161-wolai-page-ai-shell-smoke.js`
|
||||
|
||||
这一层最重要的原则不是“文案命中”,而是:
|
||||
|
||||
- 必须有截图
|
||||
- 必须能说明视口
|
||||
- 必须能说明对比对象
|
||||
- 最终仍需要人工看图
|
||||
|
||||
### 1.4 共享 helper 与登录/临时文档支撑层
|
||||
|
||||
这部分决定了 smoke 是否能稳定复用。
|
||||
|
||||
关键文件:
|
||||
|
||||
- `tree-shell-smoke-helpers.js`
|
||||
- `task114-rust-web-gateway-entry-smoke.js`
|
||||
|
||||
目前 helper 已经沉淀了几个稳定约定:
|
||||
|
||||
- 默认前端入口:`MNOTE_UI_BASE_URL`,缺省 `http://127.0.0.1:3000`
|
||||
- 默认测试账号:
|
||||
- 邮箱:`mnote.e2e@example.com`
|
||||
- 密码:`MnoteE2E123!`
|
||||
- 快捷按钮:`测试账号快速登录`
|
||||
- 通过 API 或 UI 兜底完成认证
|
||||
- 为 smoke 自动创建、重命名、清理临时页面
|
||||
|
||||
后续新脚本优先复用这些 helper,不要在每个任务里再复制一套登录和清理逻辑。
|
||||
|
||||
## 2. 当前推荐测试顺序
|
||||
|
||||
对本项目,推荐不要一上来就跑最重的 UI 对标脚本,而是按下面顺序推进。
|
||||
|
||||
### 2.1 第一步:入口守门
|
||||
|
||||
先跑入口和网关类 smoke,确认不是服务没起或路由错了。
|
||||
|
||||
建议优先:
|
||||
|
||||
```bash
|
||||
node scripts/task114-rust-web-gateway-entry-smoke.js
|
||||
node scripts/task159-auth-entry-smoke.js
|
||||
node scripts/task097-homepage-entry-smoke.js
|
||||
```
|
||||
|
||||
适用时机:
|
||||
|
||||
- 本地服务刚启动
|
||||
- 刚切换分支
|
||||
- 改了认证、入口、gateway、compat
|
||||
|
||||
### 2.2 第二步:主路径交互 smoke
|
||||
|
||||
入口没问题后,再跑和当前改动最相关的页面交互脚本。
|
||||
|
||||
例如:
|
||||
|
||||
- tree / sidebar 改动:`task112`、`task120`、`task122`
|
||||
- document shell / island:`task110`、`task121`
|
||||
- 页面设置:`task160`、`task164-page-options-visible-effect-smoke.js`
|
||||
- 双栏:`task165`
|
||||
- AI:`task155`、`task156`、`task161`、`task162`
|
||||
|
||||
原则:
|
||||
|
||||
- 只跑和当前改动相关的脚本
|
||||
- 不要无差别全量跑
|
||||
- 先窄后宽
|
||||
|
||||
### 2.3 第三步:截图证据复核
|
||||
|
||||
如果脚本本身已经输出:
|
||||
|
||||
- `screenshot`
|
||||
- `screenshotDir`
|
||||
- `measure`
|
||||
- `matrixPath`
|
||||
- `resultPath`
|
||||
|
||||
那这些产物不应只当“附带文件”,而应作为测试结果的一部分。
|
||||
|
||||
推荐做法:
|
||||
|
||||
- 成功时也保留关键截图
|
||||
- 失败时至少保留失败前最后一张截图
|
||||
- 重要 UI 任务保留整组截图目录
|
||||
|
||||
### 2.4 第四步:`/doko` 真实页面复核
|
||||
|
||||
当 smoke 已经告诉你“逻辑大体通过”,但你仍想确认页面是否真的像人看到的一样时,再补 `/doko`。
|
||||
|
||||
`/doko` 更适合回答这类问题:
|
||||
|
||||
- 首屏布局是不是顺眼
|
||||
- Sidebar 层级、图标、文案是否真可读
|
||||
- 页面设置弹层是否真的展开到位
|
||||
- 登录后工作区是否像预期渲染
|
||||
|
||||
`/doko` 不擅长替代:
|
||||
|
||||
- 稳定断言
|
||||
- 自动点击链路
|
||||
- 大量重复回归
|
||||
|
||||
所以推荐组合是:
|
||||
|
||||
`smoke 先筛出通过/失败 -> 看截图 -> 必要时再用 /doko 复核真实页面`
|
||||
|
||||
## 3. 为什么当前不建议把主线转成“浏览器插件测试”
|
||||
|
||||
当前讨论过 `codex cli + 浏览器插件` 的方向,但对 `mnote` 现阶段的主要收益并不明显。
|
||||
|
||||
原因有三点:
|
||||
|
||||
1. 你当前真正缺的是“页面直观证据”,不是“替换掉 Playwright”
|
||||
2. `scripts/` 里的 smoke 已经能覆盖多数关键交互,只是证据层不够统一
|
||||
3. 浏览器插件路线更适合复用真实 Chrome 当前标签页和真实登录环境,不是当前项目测试的首要瓶颈
|
||||
|
||||
因此,现阶段更推荐:
|
||||
|
||||
- 保留 Playwright smoke 作为自动化骨架
|
||||
- 补足截图、trace、结果摘要
|
||||
- 用 `/doko` 做人工视角复核
|
||||
|
||||
## 4. 新增或修改 smoke 的写法建议
|
||||
|
||||
这里不是要求统一重构旧脚本,而是约束新增脚本尽量沿着同一条路走。
|
||||
|
||||
### 4.1 优先复用 helper
|
||||
|
||||
优先从下面两个文件拿能力:
|
||||
|
||||
- `tree-shell-smoke-helpers.js`
|
||||
- `task114-rust-web-gateway-entry-smoke.js`
|
||||
|
||||
尤其是:
|
||||
|
||||
- 登录
|
||||
- 创建临时文档
|
||||
- 清理临时文档
|
||||
- `fetchWithTimeout`
|
||||
- `findFreePort`
|
||||
- `waitForGateway`
|
||||
|
||||
### 4.2 让脚本输出结构化结果
|
||||
|
||||
推荐脚本结束时输出 JSON,至少包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"baseUrl": "http://127.0.0.1:3000",
|
||||
"task": "taskxxx-name",
|
||||
"screenshot": "/abs/path/to/file.png",
|
||||
"screenshotDir": "/abs/path/to/dir"
|
||||
}
|
||||
```
|
||||
|
||||
如果没有截图,也应该至少输出:
|
||||
|
||||
- `ok`
|
||||
- `baseUrl`
|
||||
- `task`
|
||||
- 关键对象 id(如 `documentId`、`workspaceId`)
|
||||
|
||||
### 4.3 对 UI 任务默认保留截图
|
||||
|
||||
满足下面任一条件时,建议默认保留截图:
|
||||
|
||||
- 首屏入口变化
|
||||
- Sidebar / tree 结构变化
|
||||
- modal / popover / dropdown 变化
|
||||
- editor block 行为变化
|
||||
- Wolai 对标
|
||||
|
||||
推荐命名方式延续现有风格:
|
||||
|
||||
- `01-before-*`
|
||||
- `02-after-*`
|
||||
- `03-after-reload-*`
|
||||
|
||||
这样后续人工复核时不需要重新猜步骤。
|
||||
|
||||
### 4.4 失败信息要面向定位
|
||||
|
||||
断言报错要直接描述“哪个状态不对”,不要只写泛化失败。
|
||||
|
||||
推荐:
|
||||
|
||||
- `开启宽版后 htmlWide 应为 true,实际 false`
|
||||
- `测试账号登录后仍停留在 /auth`
|
||||
- `本地首屏未捕获工作区特征`
|
||||
|
||||
不推荐:
|
||||
|
||||
- `smoke failed`
|
||||
- `assert error`
|
||||
|
||||
## 5. 针对当前 mnote 的推荐组合
|
||||
|
||||
如果后续只是做日常开发验证,推荐这样选:
|
||||
|
||||
### 5.1 快速检查
|
||||
|
||||
适合日常改完立刻看:
|
||||
|
||||
```bash
|
||||
node scripts/task114-rust-web-gateway-entry-smoke.js
|
||||
node scripts/task159-auth-entry-smoke.js
|
||||
```
|
||||
|
||||
### 5.2 主路径回归
|
||||
|
||||
按当前改动挑 1 到 3 个最相关脚本,例如:
|
||||
|
||||
```bash
|
||||
node scripts/task120-rust-web-tree-integration-smoke.js
|
||||
node scripts/task122-rust-web-create-page-ui-smoke.js
|
||||
node scripts/task164-page-options-visible-effect-smoke.js
|
||||
```
|
||||
|
||||
### 5.3 视觉复核
|
||||
|
||||
如果任务明显涉及视觉或交互形态:
|
||||
|
||||
- 先看 smoke 产出的截图
|
||||
- 再用 `/doko` 打开真实页面复核
|
||||
|
||||
### 5.4 Wolai 对标任务
|
||||
|
||||
对标任务不要只看断言结果,至少补:
|
||||
|
||||
- 本地 smoke
|
||||
- Wolai 基线截图
|
||||
- 本地截图
|
||||
- 必要时 comparison matrix
|
||||
|
||||
## 6. 对后续演进的建议
|
||||
|
||||
这部分不是要求本轮立刻实现,而是后续维护时优先考虑。
|
||||
|
||||
### 6.1 在现有 smoke 上统一证据产物
|
||||
|
||||
优先级最高的增强不是“换工具”,而是统一输出:
|
||||
|
||||
- `tmp/.../screenshots`
|
||||
- `tmp/.../trace.zip`
|
||||
- `tmp/.../result.json`
|
||||
|
||||
这样 smoke 从“脚本 pass/fail”升级成“脚本 + 可看证据”。
|
||||
|
||||
### 6.2 把 `/doko` 固化成复核步骤
|
||||
|
||||
适合写进任务流程的话术是:
|
||||
|
||||
- 先跑 smoke
|
||||
- 再看截图
|
||||
- 仍有疑问时用 `/doko` 看真实渲染
|
||||
|
||||
不要让 `/doko` 承担自动化断言职责。
|
||||
|
||||
### 6.3 新脚本优先沿当前命名方式继续
|
||||
|
||||
目前 `taskNNN-...-smoke.js` 已经形成了可追踪的任务链,后续建议继续沿用,避免引入第二套命名体系。
|
||||
|
||||
---
|
||||
|
||||
如果后续要继续推进,最值得做的不是重写测试体系,而是:
|
||||
|
||||
1. 给最常用的 smoke 补统一截图/trace 产物
|
||||
2. 再补一份“如何读取 smoke 结果”的轻量脚本或汇总器
|
||||
|
||||
这样能直接解决当前最真实的痛点:`A + C`,也就是“结果更直观、测试更像真的看过页面”。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// 说明:
|
||||
// - 这是 task-019 的最小真实浏览器回归脚本。
|
||||
// - 目标只覆盖文档页元信息、Sidebar、BlockNote 保存链,不扩大到 Mindmap / OnlyOffice。
|
||||
// - 目标只覆盖文档页元信息、Sidebar、标题/正文保存主链,不扩大到 Mindmap / OnlyOffice。
|
||||
// - 脚本会先创建一篇临时页面,完成回归后再彻底删除,避免污染现有数据。
|
||||
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
@@ -130,7 +130,7 @@ async function createTempDocument(requestContext) {
|
||||
async function renameDocument(requestContext, documentId, workspaceId, title) {
|
||||
return await requestJson(requestContext, "/api/documents/title", {
|
||||
method: "POST",
|
||||
data: { documentId, workspaceId, title },
|
||||
data: { documentId, workspaceId, title, commandName: "page.head.updateTitle" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const {
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
async function waitForPageTitleInput(page) {
|
||||
const input = page.getByLabel("页面标题");
|
||||
const input = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
|
||||
await input.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
return input;
|
||||
}
|
||||
@@ -27,7 +27,12 @@ async function waitForTitleSave(page, documentId, expectedTitle) {
|
||||
return false;
|
||||
}
|
||||
const payload = response.request().postDataJSON();
|
||||
return payload?.documentId === documentId && payload?.title === expectedTitle && response.ok();
|
||||
return (
|
||||
payload?.documentId === documentId &&
|
||||
payload?.title === expectedTitle &&
|
||||
payload?.commandName === "page.head.updateTitle" &&
|
||||
response.ok()
|
||||
);
|
||||
},
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
@@ -56,7 +61,9 @@ async function waitForBreadcrumbTitle(page, title) {
|
||||
async function waitForSidebarRowTitle(page, documentId, title) {
|
||||
await page.waitForFunction(
|
||||
({ docId, expectedTitle }) => {
|
||||
const row = document.querySelector(`aside a[href="/documents/${docId}"]`);
|
||||
const row = document.querySelector(
|
||||
`[data-node-id="${docId}"] .tree-link-title, [data-testid="page-tree-row"][data-node-id="${docId}"], .wolai-page-row[data-node-id="${docId}"] > .wolai-row-title`,
|
||||
);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
{ docId: documentId, expectedTitle: title },
|
||||
@@ -66,12 +73,10 @@ async function waitForSidebarRowTitle(page, documentId, title) {
|
||||
|
||||
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}"]`,
|
||||
`#sidebar-tree-root .tree-row[data-shell-mode="page"][data-node-id="${nodeId}"] .tree-link-title, [data-testid="page-tree-row"][data-node-id="${nodeId}"], .wolai-page-row[data-node-id="${nodeId}"] > .wolai-row-title`,
|
||||
);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
@@ -82,12 +87,10 @@ async function waitForPageTreeTitle(page, documentId, title) {
|
||||
|
||||
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}"]`,
|
||||
`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="${docId}"] .tree-link-title, #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="${docId}"] .tree-link-title, [data-testid="filetree-doc-row"][data-doc-id="${docId}"]`,
|
||||
);
|
||||
return (row?.textContent ?? "").includes(expectedTitle);
|
||||
},
|
||||
@@ -97,7 +100,7 @@ async function waitForFileTreeTitle(page, documentId, title) {
|
||||
}
|
||||
|
||||
async function readVisibleTitle(page) {
|
||||
const titleInput = page.getByLabel("页面标题");
|
||||
const titleInput = page.locator('[data-page-title-input="true"][data-pane-role="primary"]').first();
|
||||
if (await titleInput.isVisible().catch(() => false)) {
|
||||
return (await titleInput.inputValue()).trim();
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ async function requestSseEvents(requestContext, path, body) {
|
||||
async function fetchPageAggregate(requestContext, documentId, workspaceId) {
|
||||
return await requestJson(
|
||||
requestContext,
|
||||
`/api/documents/page?documentId=${encodeURIComponent(documentId)}&workspaceId=${encodeURIComponent(workspaceId)}`,
|
||||
`/api/page-aggregate/${encodeURIComponent(documentId)}?workspaceId=${encodeURIComponent(workspaceId)}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ async function fetchDocumentContent(requestContext, documentId, workspaceId) {
|
||||
}
|
||||
|
||||
function buildAiContextFromPageAggregate(pagePayload) {
|
||||
const page = pagePayload?.page ?? null;
|
||||
const page = pagePayload?.result ?? null;
|
||||
const pageSubtree = page?.tree?.pageSubtree ?? null;
|
||||
const subtreeNodes = Array.isArray(pageSubtree?.subtree?.nodes)
|
||||
? pageSubtree.subtree.nodes.slice(0, 160)
|
||||
|
||||
@@ -353,7 +353,12 @@ async function renameThroughPageHead(page, documentId, title) {
|
||||
return false;
|
||||
}
|
||||
const payload = response.request().postDataJSON();
|
||||
return payload?.documentId === documentId && payload?.title === title && response.ok();
|
||||
return (
|
||||
payload?.documentId === documentId &&
|
||||
payload?.title === title &&
|
||||
payload?.commandName === "page.head.updateTitle" &&
|
||||
response.ok()
|
||||
);
|
||||
},
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ async function validateSkipNextRuntimePlan() {
|
||||
const plan = resolveRuntimePlan({
|
||||
FRONTEND_PORT: "3000",
|
||||
NEXT_LEGACY_PORT: "3100",
|
||||
SKIP_NEXT_LEGACY: "1",
|
||||
MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT: "1",
|
||||
});
|
||||
assert.equal(plan.skipGateway, false);
|
||||
assert.equal(plan.skipNextLegacy, true);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const assert = require("node:assert");
|
||||
const { chromium } = require("playwright");
|
||||
const { fetchWithTimeout } = require("./task114-rust-web-gateway-entry-smoke.js");
|
||||
const { createTempDocument, ensureAuthenticated, openDocument } = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
@@ -36,13 +37,17 @@ 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 seed = null;
|
||||
let created = null;
|
||||
|
||||
try {
|
||||
const response = await page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
await ensureAuthenticated(page, context.request);
|
||||
seed = await createTempDocument(context.request, null);
|
||||
const targetUrl = await openDocument(page, seed.workspaceId, seed.documentId);
|
||||
const response = await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
assert(response, "根页没有返回响应");
|
||||
assert.equal(response.status(), 200, `根页状态码异常: ${response.status()}`);
|
||||
assert.equal(response.headers()["x-mnote-web-owner"], "mnote-web", "根页必须由 mnote-web 拥有");
|
||||
assert.equal(response.headers()["x-mnote-web-owner"], "mnote-web", "文档页必须由 mnote-web 拥有");
|
||||
|
||||
const createButton = page.getByTestId("wolai-sidebar-create-page").first();
|
||||
await createButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
@@ -59,7 +64,7 @@ async function main() {
|
||||
assert(workspaceId, "新建后 URL 缺少 workspaceId");
|
||||
created = { documentId, workspaceId };
|
||||
|
||||
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"]').waitFor({
|
||||
await page.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"][data-pane-role="primary"]').waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
});
|
||||
@@ -90,6 +95,12 @@ async function main() {
|
||||
`清理 UI 新建页面 ${created.documentId}`,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
if (seed?.documentId && seed?.workspaceId) {
|
||||
await postTreeCommand(
|
||||
{ action: "purge", workspaceId: seed.workspaceId, documentId: seed.documentId },
|
||||
`清理种子页面 ${seed.documentId}`,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
await page.close().catch(() => undefined);
|
||||
await context.close().catch(() => undefined);
|
||||
await browser.close().catch(() => undefined);
|
||||
|
||||
@@ -331,7 +331,11 @@ async function run() {
|
||||
const titleResponse = await fetch("/api/documents/title", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...common, title: "Browser Saved Title" }),
|
||||
body: JSON.stringify({
|
||||
...common,
|
||||
title: "Browser Saved Title",
|
||||
commandName: "page.head.updateTitle",
|
||||
}),
|
||||
});
|
||||
if (!titleResponse.ok) throw new Error(`title_failed_${titleResponse.status}`);
|
||||
const saveResponse = await fetch("/api/documents/save", {
|
||||
|
||||
@@ -4,7 +4,7 @@ const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").repl
|
||||
const DOCUMENT_UI_BASE_URL = (
|
||||
process.env.MNOTE_TREE_SMOKE_DOCUMENT_BASE_URL ||
|
||||
process.env.MNOTE_LEGACY_UI_BASE_URL ||
|
||||
"http://127.0.0.1:3100"
|
||||
BASE_URL
|
||||
).replace(/\/+$/, "");
|
||||
const MNOTE_WEB_BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "").replace(/\/+$/, "");
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
|
||||
@@ -109,6 +109,7 @@ async function renameDocument(requestContext, workspaceId, documentId, title) {
|
||||
workspaceId,
|
||||
documentId,
|
||||
title,
|
||||
commandName: "page.head.updateTitle",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -339,9 +340,26 @@ async function cleanupDocuments(requestContext, createdIds) {
|
||||
async function openDocument(page, workspaceId, documentId) {
|
||||
const url = `${DOCUMENT_UI_BASE_URL}/documents/${documentId}?workspaceId=${encodeURIComponent(workspaceId)}`;
|
||||
const hasSidebarControls = async () => {
|
||||
const pageTab = page.locator('[data-mnote-sidebar-tree-tab="page"]').first();
|
||||
const filetreeTab = page.locator('[data-mnote-sidebar-tree-tab="filetree"]').first();
|
||||
const groupButton = page.getByRole("button", { name: "分组" });
|
||||
const fileButton = page.getByRole("button", { name: "文件" });
|
||||
return (await isVisible(groupButton)) || (await isVisible(fileButton));
|
||||
if ((await isVisible(pageTab)) || (await isVisible(filetreeTab))) {
|
||||
return true;
|
||||
}
|
||||
if ((await isVisible(groupButton)) || (await isVisible(fileButton))) {
|
||||
return true;
|
||||
}
|
||||
return await page.evaluate(() => {
|
||||
const pageShell = document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
|
||||
const legacyShell = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
return [pageShell, legacyShell].some((node) => {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
const rect = node.getBoundingClientRect();
|
||||
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const gotoDocument = async () => {
|
||||
@@ -379,13 +397,20 @@ async function openDocument(page, workspaceId, documentId) {
|
||||
}
|
||||
|
||||
async function openSectionView(page) {
|
||||
const button = page.getByRole("button", { name: "分组" });
|
||||
await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const waitForHost = () =>
|
||||
page.waitForFunction(
|
||||
() => {
|
||||
const pageHost = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
return pageHost instanceof HTMLElement && pageHost.getClientRects().length > 0;
|
||||
const pagePanel = document.querySelector('[data-mnote-sidebar-tree-panel="page"]');
|
||||
const pageRoot = document.getElementById("sidebar-tree-root");
|
||||
const legacyHost = document.querySelector('[data-testid="sidebar-page-tree-shell"]');
|
||||
const isVisible = (node) =>
|
||||
node instanceof HTMLElement &&
|
||||
!node.hidden &&
|
||||
window.getComputedStyle(node).display !== "none" &&
|
||||
window.getComputedStyle(node).visibility !== "hidden" &&
|
||||
node.getClientRects().length > 0;
|
||||
if (isVisible(legacyHost)) return true;
|
||||
return isVisible(pageRoot) && (!pagePanel || isVisible(pagePanel));
|
||||
},
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
@@ -399,7 +424,23 @@ async function openSectionView(page) {
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await clickButtonByExactText(page, "分组");
|
||||
const switched = await page.evaluate(() => {
|
||||
const selectors = [
|
||||
'[data-mnote-sidebar-tree-tab="page"]',
|
||||
'button[aria-label="分组"]',
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node instanceof HTMLElement) {
|
||||
node.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!switched) {
|
||||
throw new Error("未找到页面树切换入口");
|
||||
}
|
||||
try {
|
||||
await waitForHost();
|
||||
return;
|
||||
@@ -412,13 +453,20 @@ async function openSectionView(page) {
|
||||
}
|
||||
|
||||
async function openFilesystemView(page) {
|
||||
const button = page.getByRole("button", { name: "文件" });
|
||||
await button.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const waitForHost = () =>
|
||||
page.waitForFunction(
|
||||
() => {
|
||||
const fileHost = document.querySelector('[data-testid="sidebar-file-tree-shell"]');
|
||||
return fileHost instanceof HTMLElement && fileHost.getClientRects().length > 0;
|
||||
const filePanel = document.querySelector('[data-mnote-sidebar-tree-panel="filetree"]');
|
||||
const fileRoot = document.getElementById("sidebar-file-tree-root");
|
||||
const legacyHost = document.querySelector('[data-testid="sidebar-file-tree-shell"]');
|
||||
const isVisible = (node) =>
|
||||
node instanceof HTMLElement &&
|
||||
!node.hidden &&
|
||||
window.getComputedStyle(node).display !== "none" &&
|
||||
window.getComputedStyle(node).visibility !== "hidden" &&
|
||||
node.getClientRects().length > 0;
|
||||
if (isVisible(legacyHost)) return true;
|
||||
return isVisible(fileRoot) && (!filePanel || isVisible(filePanel));
|
||||
},
|
||||
undefined,
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
@@ -432,7 +480,23 @@ async function openFilesystemView(page) {
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await clickButtonByExactText(page, "文件");
|
||||
const switched = await page.evaluate(() => {
|
||||
const selectors = [
|
||||
'[data-mnote-sidebar-tree-tab="filetree"]',
|
||||
'button[aria-label="文件"]',
|
||||
];
|
||||
for (const selector of selectors) {
|
||||
const node = document.querySelector(selector);
|
||||
if (node instanceof HTMLElement) {
|
||||
node.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!switched) {
|
||||
throw new Error("未找到文件树切换入口");
|
||||
}
|
||||
try {
|
||||
await waitForHost();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user