refactor: extract dnd target and sidebar helpers
This commit is contained in:
@@ -28,8 +28,10 @@ const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000
|
||||
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "task487-local-folder-tree-live-consumer-smoke");
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "result.json");
|
||||
const SCREENSHOT_BEFORE_PATH = path.join(OUTPUT_DIR, "page-tree-before-create.png");
|
||||
const SCREENSHOT_AFTER_PATH = path.join(OUTPUT_DIR, "page-tree-after-create.png");
|
||||
const SCREENSHOT_PAGE_TREE_LOADED_PATH = path.join(OUTPUT_DIR, "page-tree-loaded.png");
|
||||
const SCREENSHOT_PAGE_TREE_AFTER_CREATE_PATH = path.join(OUTPUT_DIR, "page-tree-after-create.png");
|
||||
const SCREENSHOT_PAGE_TREE_AFTER_DELETE_PATH = path.join(OUTPUT_DIR, "page-tree-after-delete.png");
|
||||
const SCREENSHOT_FILETREE_AFTER_CREATE_PATH = path.join(OUTPUT_DIR, "filetree-after-create.png");
|
||||
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
@@ -75,21 +77,52 @@ function writeWorkspaceManifest(root, ownerId) {
|
||||
|
||||
/**
|
||||
* 读取 <html> 上的所有 data-mnote-tree-live-* 属性和
|
||||
* data-mnote-local-folder-watch-applied,返回一个状态对象。
|
||||
* data-mnote-local-folder-watch-applied,并补充当前树 DOM 证据。
|
||||
*/
|
||||
async function readLiveDomState(page) {
|
||||
return page.evaluate(() => {
|
||||
async function readLiveDomState(page, evidence = {}) {
|
||||
return page.evaluate((options) => {
|
||||
const html = document.documentElement;
|
||||
const pageTreeRows = Array.from(document.querySelectorAll("#sidebar-tree-root .tree-row[data-node-id]"));
|
||||
const fileTreeRows = Array.from(document.querySelectorAll(".tree-row[data-row-id]"));
|
||||
const targetNodeId = options.targetNodeId || "";
|
||||
const targetRowSelector = options.targetRowSelector || "";
|
||||
const targetNodePresent = targetNodeId
|
||||
? pageTreeRows.some((row) => row.getAttribute("data-node-id") === targetNodeId)
|
||||
: null;
|
||||
const targetRowPresent = targetRowSelector
|
||||
? !!document.querySelector(targetRowSelector)
|
||||
: null;
|
||||
|
||||
return {
|
||||
label: options.label || "",
|
||||
treeLiveTransport: html.getAttribute("data-mnote-tree-live-transport") || "",
|
||||
treeLiveStatus: html.getAttribute("data-mnote-tree-live-status") || "",
|
||||
treeLiveApplied: html.getAttribute("data-mnote-tree-live-applied") || "",
|
||||
treeLiveRevision: html.getAttribute("data-mnote-tree-live-revision") || "",
|
||||
treeLiveApplyError: html.getAttribute("data-mnote-tree-live-apply-error") || "",
|
||||
localFolderWatchApplied: html.getAttribute("data-mnote-local-folder-watch-applied") || "",
|
||||
pageTreeRowCount: pageTreeRows.length,
|
||||
fileTreeRowCount: fileTreeRows.length,
|
||||
targetNodeId,
|
||||
targetNodePresent,
|
||||
targetRowSelector,
|
||||
targetRowPresent,
|
||||
url: window.location.href,
|
||||
};
|
||||
});
|
||||
}, evidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一个阶段内是否发生主框架导航。RED smoke 不因失败退出非 0,
|
||||
* 但需要把 reload / full navigation 作为明确证据写入 result.json。
|
||||
*/
|
||||
function navigationEvidence(label, events) {
|
||||
return {
|
||||
label,
|
||||
pass: events.length === 0,
|
||||
count: events.length,
|
||||
events: events.slice(0, 20),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,9 +177,14 @@ async function run() {
|
||||
});
|
||||
|
||||
const assertions = [];
|
||||
let domStateBefore = null;
|
||||
let domStateAfterCreate = null;
|
||||
let domStateAfterDelete = null;
|
||||
let pageTreeLoadedState = null;
|
||||
let pageTreeAfterCreateState = null;
|
||||
let pageTreeAfterDeleteState = null;
|
||||
let filetreeLoadedState = null;
|
||||
let filetreeAfterCreateState = null;
|
||||
let filetreeAfterDeleteState = null;
|
||||
let pageTreeNavigationEvidence = null;
|
||||
let filetreeNavigationEvidence = null;
|
||||
let fatalError = null;
|
||||
|
||||
try {
|
||||
@@ -173,72 +211,77 @@ async function run() {
|
||||
navigationEvents.length = 0;
|
||||
|
||||
// 记录加载后 DOM 状态
|
||||
domStateBefore = await readLiveDomState(page);
|
||||
pageTreeLoadedState = await readLiveDomState(page, { label: "pageTreeLoaded" });
|
||||
|
||||
// 断言 1: transport 不应为 local-folder-static
|
||||
assertions.push(check(
|
||||
"data-mnote-tree-live-transport !== 'local-folder-static'",
|
||||
domStateBefore.treeLiveTransport !== "local-folder-static",
|
||||
pageTreeLoadedState.treeLiveTransport !== "local-folder-static",
|
||||
'非 local-folder-static(期望 "local-folder-events" 或等价)',
|
||||
domStateBefore.treeLiveTransport,
|
||||
pageTreeLoadedState.treeLiveTransport,
|
||||
));
|
||||
|
||||
// 断言 2: status 不应为 static
|
||||
assertions.push(check(
|
||||
"data-mnote-tree-live-status 不为 static",
|
||||
domStateBefore.treeLiveStatus !== "static",
|
||||
pageTreeLoadedState.treeLiveStatus !== "static",
|
||||
"connected / connecting / disabled 等非 static 值",
|
||||
domStateBefore.treeLiveStatus,
|
||||
pageTreeLoadedState.treeLiveStatus,
|
||||
));
|
||||
|
||||
// 断言 3: 有 transport 属性(不是空)
|
||||
assertions.push(check(
|
||||
"data-mnote-tree-live-transport 非空",
|
||||
domStateBefore.treeLiveTransport !== "",
|
||||
pageTreeLoadedState.treeLiveTransport !== "",
|
||||
"非空字符串",
|
||||
domStateBefore.treeLiveTransport,
|
||||
pageTreeLoadedState.treeLiveTransport,
|
||||
));
|
||||
|
||||
// 截图:page tree 加载后状态
|
||||
await page.screenshot({ path: SCREENSHOT_BEFORE_PATH, fullPage: false });
|
||||
await page.screenshot({ path: SCREENSHOT_PAGE_TREE_LOADED_PATH, fullPage: false });
|
||||
|
||||
// 断言 4: 外部文件创建后,tree live applied 应为 snapshot 或 resync
|
||||
// 先确认页面稳定
|
||||
await page.waitForTimeout(200);
|
||||
domStateBefore = await readLiveDomState(page);
|
||||
pageTreeLoadedState = await readLiveDomState(page, { label: "pageTreeLoadedAfterStabilize" });
|
||||
|
||||
// 创建外部文件
|
||||
const createdFileRel = "docs/live-consumer-test.md";
|
||||
const createdFileDocumentId = localMdDocumentId(createdFileRel);
|
||||
fs.writeFileSync(path.join(root, createdFileRel), "# Live Consumer Test\n", "utf8");
|
||||
|
||||
// 等待 page tree row 出现(当前通过 polling 机制)
|
||||
await waitForPageTreeNode(page, localMdDocumentId(createdFileRel)).catch(() => {});
|
||||
await waitForPageTreeNode(page, createdFileDocumentId).catch(() => {});
|
||||
|
||||
// 等待一小段时间让 tree live consumer 有机会触发
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
domStateAfterCreate = await readLiveDomState(page);
|
||||
pageTreeAfterCreateState = await readLiveDomState(page, {
|
||||
label: "pageTreeAfterExternalCreate",
|
||||
targetNodeId: createdFileDocumentId,
|
||||
});
|
||||
await page.screenshot({ path: SCREENSHOT_PAGE_TREE_AFTER_CREATE_PATH, fullPage: false });
|
||||
|
||||
// 断言 4: applied 应为 snapshot 或 resync
|
||||
const appliedOk = domStateAfterCreate.treeLiveApplied === "snapshot"
|
||||
|| domStateAfterCreate.treeLiveApplied === "resync";
|
||||
const appliedOk = pageTreeAfterCreateState.treeLiveApplied === "snapshot"
|
||||
|| pageTreeAfterCreateState.treeLiveApplied === "resync";
|
||||
assertions.push(check(
|
||||
"外部创建后 data-mnote-tree-live-applied 为 snapshot|resync",
|
||||
appliedOk,
|
||||
"snapshot 或 resync",
|
||||
domStateAfterCreate.treeLiveApplied,
|
||||
pageTreeAfterCreateState.treeLiveApplied,
|
||||
));
|
||||
|
||||
// 断言 5: watch-applied 不再是唯一证据
|
||||
// 即 treeLiveApplied 存在(非空)或 transport 不是 local-folder-static
|
||||
// 如果 watch-applied 是唯一标记而 treeLiveApplied 为空,则不合格
|
||||
const hasTreeLiveEvidence = domStateAfterCreate.treeLiveApplied !== ""
|
||||
|| domStateAfterCreate.treeLiveTransport !== "local-folder-static";
|
||||
const hasTreeLiveEvidence = pageTreeAfterCreateState.treeLiveApplied !== ""
|
||||
|| pageTreeAfterCreateState.treeLiveTransport !== "local-folder-static";
|
||||
assertions.push(check(
|
||||
"存在 tree live consumer 证据(非仅 local-folder-watch-applied)",
|
||||
hasTreeLiveEvidence,
|
||||
"treeLiveApplied 非空 或 transport !== local-folder-static",
|
||||
`treeLiveApplied="${domStateAfterCreate.treeLiveApplied}" transport="${domStateAfterCreate.treeLiveTransport}"`,
|
||||
`treeLiveApplied="${pageTreeAfterCreateState.treeLiveApplied}" transport="${pageTreeAfterCreateState.treeLiveTransport}"`,
|
||||
));
|
||||
|
||||
// 断言 6: 外部文件删除后同样有 tree live applied 痕迹
|
||||
@@ -250,19 +293,28 @@ async function run() {
|
||||
{ timeout: UI_TIMEOUT_MS },
|
||||
).catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
domStateAfterDelete = await readLiveDomState(page);
|
||||
pageTreeAfterDeleteState = await readLiveDomState(page, {
|
||||
label: "pageTreeAfterExternalDelete",
|
||||
targetNodeId: createdFileDocumentId,
|
||||
});
|
||||
await page.screenshot({ path: SCREENSHOT_PAGE_TREE_AFTER_DELETE_PATH, fullPage: false });
|
||||
|
||||
const appliedDelOk = domStateAfterDelete.treeLiveApplied === "snapshot"
|
||||
|| domStateAfterDelete.treeLiveApplied === "resync";
|
||||
const appliedDelOk = pageTreeAfterDeleteState.treeLiveApplied === "snapshot"
|
||||
|| pageTreeAfterDeleteState.treeLiveApplied === "resync";
|
||||
assertions.push(check(
|
||||
"外部删除后 data-mnote-tree-live-applied 为 snapshot|resync",
|
||||
appliedDelOk,
|
||||
"snapshot 或 resync",
|
||||
domStateAfterDelete.treeLiveApplied,
|
||||
pageTreeAfterDeleteState.treeLiveApplied,
|
||||
));
|
||||
|
||||
// 截图:外部创建后
|
||||
await page.screenshot({ path: SCREENSHOT_AFTER_PATH, fullPage: false });
|
||||
pageTreeNavigationEvidence = navigationEvidence("PageTree 外部创建/删除阶段无主框架导航或 reload", navigationEvents);
|
||||
assertions.push(check(
|
||||
pageTreeNavigationEvidence.label,
|
||||
pageTreeNavigationEvidence.pass,
|
||||
"navigationEvents.length === 0",
|
||||
pageTreeNavigationEvidence.count,
|
||||
));
|
||||
|
||||
// ── Phase 2: FileTree 视图 ──
|
||||
await page.goto(documentUrl(root, "README.md", "filetree"), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
||||
@@ -277,38 +329,71 @@ async function run() {
|
||||
await page.waitForTimeout(300);
|
||||
navigationEvents.length = 0;
|
||||
|
||||
const filetreeState = await readLiveDomState(page);
|
||||
filetreeLoadedState = await readLiveDomState(page, { label: "filetreeLoaded" });
|
||||
|
||||
assertions.push(check(
|
||||
"FileTree: transport !== 'local-folder-static'",
|
||||
filetreeState.treeLiveTransport !== "local-folder-static",
|
||||
filetreeLoadedState.treeLiveTransport !== "local-folder-static",
|
||||
"非 local-folder-static",
|
||||
filetreeState.treeLiveTransport,
|
||||
filetreeLoadedState.treeLiveTransport,
|
||||
));
|
||||
|
||||
assertions.push(check(
|
||||
"FileTree: status 不为 static",
|
||||
filetreeState.treeLiveStatus !== "static",
|
||||
filetreeLoadedState.treeLiveStatus !== "static",
|
||||
"非 static",
|
||||
filetreeState.treeLiveStatus,
|
||||
filetreeLoadedState.treeLiveStatus,
|
||||
));
|
||||
|
||||
// FileTree 外部创建
|
||||
fs.writeFileSync(path.join(root, "docs", "filetree-live-test.md"), "# FileTree Live\n", "utf8");
|
||||
const filetreeCreatedRel = "docs/filetree-live-test.md";
|
||||
const filetreeCreatedRowSelector = `.tree-row[data-row-id="local:markdown:${filetreeCreatedRel}"]`;
|
||||
fs.writeFileSync(path.join(root, filetreeCreatedRel), "# FileTree Live\n", "utf8");
|
||||
// 等待 filetree row 出现(与 task435 同模式)
|
||||
await page.locator(`.tree-row[data-row-id="local:markdown:docs/filetree-live-test.md"]`).waitFor({
|
||||
await page.locator(filetreeCreatedRowSelector).waitFor({
|
||||
state: "visible",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
}).catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
const filetreeAfterCreate = await readLiveDomState(page);
|
||||
filetreeAfterCreateState = await readLiveDomState(page, {
|
||||
label: "filetreeAfterExternalCreate",
|
||||
targetRowSelector: filetreeCreatedRowSelector,
|
||||
});
|
||||
await page.screenshot({ path: SCREENSHOT_FILETREE_AFTER_CREATE_PATH, fullPage: false });
|
||||
|
||||
assertions.push(check(
|
||||
"FileTree 外部创建后 tree-live-applied 为 snapshot|resync",
|
||||
filetreeAfterCreate.treeLiveApplied === "snapshot" || filetreeAfterCreate.treeLiveApplied === "resync",
|
||||
filetreeAfterCreateState.treeLiveApplied === "snapshot" || filetreeAfterCreateState.treeLiveApplied === "resync",
|
||||
"snapshot 或 resync",
|
||||
filetreeAfterCreate.treeLiveApplied,
|
||||
filetreeAfterCreateState.treeLiveApplied,
|
||||
));
|
||||
|
||||
fs.rmSync(path.join(root, filetreeCreatedRel));
|
||||
await page.locator(filetreeCreatedRowSelector).waitFor({
|
||||
state: "detached",
|
||||
timeout: UI_TIMEOUT_MS,
|
||||
}).catch(() => {});
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
filetreeAfterDeleteState = await readLiveDomState(page, {
|
||||
label: "filetreeAfterExternalDelete",
|
||||
targetRowSelector: filetreeCreatedRowSelector,
|
||||
});
|
||||
|
||||
assertions.push(check(
|
||||
"FileTree 外部删除后 tree-live-applied 为 snapshot|resync",
|
||||
filetreeAfterDeleteState.treeLiveApplied === "snapshot" || filetreeAfterDeleteState.treeLiveApplied === "resync",
|
||||
"snapshot 或 resync",
|
||||
filetreeAfterDeleteState.treeLiveApplied,
|
||||
));
|
||||
|
||||
filetreeNavigationEvidence = navigationEvidence("FileTree 外部创建/删除阶段无主框架导航或 reload", navigationEvents);
|
||||
assertions.push(check(
|
||||
filetreeNavigationEvidence.label,
|
||||
filetreeNavigationEvidence.pass,
|
||||
"navigationEvents.length === 0",
|
||||
filetreeNavigationEvidence.count,
|
||||
));
|
||||
|
||||
} catch (err) {
|
||||
@@ -325,13 +410,27 @@ async function run() {
|
||||
? "✅ 所有断言通过 — local_folder 已收敛到 tree live consumer"
|
||||
: "🔴 RED — 一个或多个断言失败,local_folder 仍使用 local-folder-static / polling 主链",
|
||||
assertions,
|
||||
domStateBefore,
|
||||
domStateAfterCreate,
|
||||
domStateAfterDelete,
|
||||
domStateBefore: pageTreeLoadedState,
|
||||
domStateAfterCreate: pageTreeAfterCreateState,
|
||||
domStateAfterDelete: pageTreeAfterDeleteState,
|
||||
stagedDomStates: {
|
||||
pageTreeLoaded: pageTreeLoadedState,
|
||||
pageTreeAfterExternalCreate: pageTreeAfterCreateState,
|
||||
pageTreeAfterExternalDelete: pageTreeAfterDeleteState,
|
||||
filetreeLoaded: filetreeLoadedState,
|
||||
filetreeAfterExternalCreate: filetreeAfterCreateState,
|
||||
filetreeAfterExternalDelete: filetreeAfterDeleteState,
|
||||
},
|
||||
navigationEvidence: {
|
||||
pageTreeExternalMutations: pageTreeNavigationEvidence,
|
||||
filetreeExternalMutations: filetreeNavigationEvidence,
|
||||
},
|
||||
navigationEvents: navigationEvents.slice(0, 20),
|
||||
screenshots: {
|
||||
pageTreeLoaded: SCREENSHOT_BEFORE_PATH,
|
||||
afterExternalCreate: SCREENSHOT_AFTER_PATH,
|
||||
pageTreeLoaded: SCREENSHOT_PAGE_TREE_LOADED_PATH,
|
||||
pageTreeAfterExternalCreate: SCREENSHOT_PAGE_TREE_AFTER_CREATE_PATH,
|
||||
pageTreeAfterExternalDelete: SCREENSHOT_PAGE_TREE_AFTER_DELETE_PATH,
|
||||
filetreeAfterExternalCreate: SCREENSHOT_FILETREE_AFTER_CREATE_PATH,
|
||||
},
|
||||
fatalError,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user