From cd7fb392f6343906bc5064c427d781af87874f0e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Jul 2026 14:23:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(web-probe):=20=E5=AF=B9=E9=BD=90=E6=96=B0?= =?UTF-8?q?=E7=89=88=20MDTODO=20DOM=20=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...web-observe-runner-sampling-source.test.ts | 31 ++++++++++++-- ...node-web-observe-runner-sampling-source.ts | 31 +++++++++----- ...eb-observe-runner-workbench-source.test.ts | 38 +++++++++++++++++ ...ode-web-observe-runner-workbench-source.ts | 42 ++++++++++++------- 4 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 scripts/src/hwlab-node-web-observe-runner-workbench-source.test.ts diff --git a/scripts/src/hwlab-node-web-observe-runner-sampling-source.test.ts b/scripts/src/hwlab-node-web-observe-runner-sampling-source.test.ts index 134d21c1..229028d2 100644 --- a/scripts/src/hwlab-node-web-observe-runner-sampling-source.test.ts +++ b/scripts/src/hwlab-node-web-observe-runner-sampling-source.test.ts @@ -7,10 +7,32 @@ import { } from "./hwlab-node-web-observe-runner-sampling-source"; test("MDTODO task tree pane gap treats the pinned facts footer as content", async () => { - assert.match(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR, /\[data-testid="mdtodo-task-tree-facts"\]/u); + assert.match( + MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR, + /\[data-testid="mdtodo-task-tree-facts"\]/u, + ); + assert.match( + MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR, + /\[data-testid="mdtodo-task-page-fact"\]/u, + ); const source = nodeWebObserveRunnerSamplingSource(); - assert.ok(source.includes(`measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)})`)); + assert.ok( + source.includes( + `measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)})`, + ), + ); + assert.match( + source, + /semanticTaskCandidates\.length > 0 \? semanticTaskCandidates : fallbackTaskCandidates/u, + ); + assert.match( + source, + /taskCandidates\.filter\(\(candidate\) => !taskRefElement\(candidate\)\)\.length/u, + ); + assert.doesNotMatch(source, /taskCandidates\.length - taskItems\.length/u); + assert.match(source, /mdtodo-body-read/u); + assert.match(source, /mdtodo-launch-workbench/u); const child = Bun.spawn(["node", "--input-type=module", "--check", "-"], { stdin: "pipe", @@ -19,6 +41,9 @@ test("MDTODO task tree pane gap treats the pinned facts footer as content", asyn }); child.stdin.write(source); child.stdin.end(); - const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]); + const [stderr, exitCode] = await Promise.all([ + new Response(child.stderr).text(), + child.exited, + ]); assert.equal(exitCode, 0, stderr); }); diff --git a/scripts/src/hwlab-node-web-observe-runner-sampling-source.ts b/scripts/src/hwlab-node-web-observe-runner-sampling-source.ts index 77a6b53c..b3b1491f 100644 --- a/scripts/src/hwlab-node-web-observe-runner-sampling-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-sampling-source.ts @@ -3,6 +3,7 @@ export const MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR = [ '[data-testid="mdtodo-task-tree-facts"]', + '[data-testid="mdtodo-task-page-fact"]', "[data-task-ref]", '[role="treeitem"]', '[role="listitem"]', @@ -433,22 +434,32 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag selected: option.selected === true, })) : []; const selectedFileOption = fileOptions.find((option) => option.selected) || null; - const taskItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]')).filter(visible); - const taskCandidates = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] li, [data-testid="mdtodo-task-tree"] [role="treeitem"], [data-testid="mdtodo-task-tree"] [role="listitem"]')).filter(visible); + const taskTree = document.querySelector('[data-testid="mdtodo-task-tree"]'); + const semanticTaskCandidates = taskTree + ? Array.from(taskTree.querySelectorAll('[role="treeitem"], [role="listitem"]')).filter(visible) + : []; + const fallbackTaskCandidates = taskTree + ? Array.from(taskTree.querySelectorAll("li")).filter(visible) + : []; + const taskCandidates = semanticTaskCandidates.length > 0 ? semanticTaskCandidates : fallbackTaskCandidates; + const taskRefElement = (candidate) => candidate.matches("[data-task-ref]") + ? candidate + : candidate.querySelector("[data-task-ref]"); + const taskItems = Array.from(new Set(taskCandidates.map(taskRefElement).filter(visible))); const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected'); const selectedFile = document.querySelector('[data-file-ref][data-selected="true"], [data-file-ref][aria-selected="true"], [data-file-ref].selected, [data-file-ref].is-selected'); const selectedTask = document.querySelector('[data-task-ref][data-selected="true"], [data-task-ref][aria-selected="true"], [data-task-ref].selected, [data-task-ref].is-selected'); const statusCounts = {}; for (const task of taskItems) { - const status = task.getAttribute("data-task-status") || "unknown"; + const status = task.getAttribute("data-task-status") || task.querySelector("[data-state]")?.getAttribute("data-state") || "unknown"; statusCounts[status] = (statusCounts[status] || 0) + 1; } - const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]'); - const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]'); + const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]'); + const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"], [data-testid="mdtodo-body-read"]'); const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]'); const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]'); const reportLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-report-link"]')).filter(visible); - const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({ + const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [data-testid="mdtodo-create-blocker"], .launch-blocker, [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({ index, testId: element.getAttribute("data-testid"), role: element.getAttribute("role"), @@ -475,7 +486,7 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag }; const paneGaps = [ measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)}), - measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-document-footer'), + measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-body-read"], [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .document-header, .task-status-stack > *, .task-document-footer, .document-footer'), measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'), ]; return { @@ -486,17 +497,17 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag sourceCount: Math.max(sourceItems.length, sourceOptionCount), fileCount: Math.max(fileItems.length, fileOptionCount), taskCount: taskItems.length, - taskRefMissingCount: Math.max(0, taskCandidates.length - taskItems.length), + taskRefMissingCount: taskCandidates.filter((candidate) => !taskRefElement(candidate)).length, selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value), selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value), selectedFileLabel: selectedFile ? trim(selectedFile.textContent || "", 180) : selectedFileOption?.label || null, fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 24), selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")), - selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null, + selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || selectedTask?.querySelector("[data-state]")?.getAttribute("data-state") || null, sourceSelectVisible: visible(sourceSelect), fileSelectVisible: visible(fileSelect), sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')), - taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')), + taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"], [data-testid="mdtodo-body-editor"]')), taskBodyVisible: visible(bodyRendered), taskBodyText: visible(bodyRendered) ? trim(bodyRendered.textContent || "", 500) : "", newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')), diff --git a/scripts/src/hwlab-node-web-observe-runner-workbench-source.test.ts b/scripts/src/hwlab-node-web-observe-runner-workbench-source.test.ts new file mode 100644 index 00000000..cab7e6ca --- /dev/null +++ b/scripts/src/hwlab-node-web-observe-runner-workbench-source.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { test } from "bun:test"; + +import { nodeWebObserveRunnerWorkbenchSource } from "./hwlab-node-web-observe-runner-workbench-source"; + +test("MDTODO observe commands recognize the rewritten workspace contracts", async () => { + const source = nodeWebObserveRunnerWorkbenchSource(); + + for (const testId of [ + "mdtodo-body-read", + "mdtodo-body-editor", + "mdtodo-body-save", + "mdtodo-launch-workbench", + ]) { + assert.match(source, new RegExp(testId, "u")); + } + for (const legacyTestId of [ + "mdtodo-body-rendered", + "mdtodo-edit-body", + "mdtodo-edit-body-save", + "mdtodo-workbench-launch", + ]) { + assert.match(source, new RegExp(legacyTestId, "u")); + } + + const child = Bun.spawn(["node", "--input-type=module", "--check", "-"], { + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + child.stdin.write(source); + child.stdin.end(); + const [stderr, exitCode] = await Promise.all([ + new Response(child.stderr).text(), + child.exited, + ]); + assert.equal(exitCode, 0, stderr); +}); diff --git a/scripts/src/hwlab-node-web-observe-runner-workbench-source.ts b/scripts/src/hwlab-node-web-observe-runner-workbench-source.ts index f2d0a9cf..3267df49 100644 --- a/scripts/src/hwlab-node-web-observe-runner-workbench-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-workbench-source.ts @@ -764,7 +764,7 @@ async function selectMdtodoTask(command) { const clicked = await target.locator.evaluate((element) => ({ taskRef: element.getAttribute("data-task-ref") || null, taskId: element.getAttribute("data-task-id") || element.getAttribute("data-rxx-id") || null, - status: element.getAttribute("data-task-status") || null, + status: element.getAttribute("data-task-status") || element.querySelector("[data-state]")?.getAttribute("data-state") || null, selected: element.getAttribute("data-selected") === "true" || element.getAttribute("aria-selected") === "true" })).catch(() => ({ taskRef: target.taskRef || null, taskId: target.taskId || null, status: null })); if (!clicked.selected) { @@ -895,12 +895,22 @@ async function closeMdtodoSourceConfigIfOpen(options = {}) { return result; } +function mdtodoTestIdCandidates(value) { + return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" && item.length > 0); +} + +function mdtodoTestIdLocator(value) { + const candidates = mdtodoTestIdCandidates(value); + return page.locator(candidates.map((testId) => '[data-testid="' + cssEscape(testId) + '"]').join(", ")).first(); +} + async function fillMdtodoField(testId, value) { if (typeof value !== "string" || !value.trim()) return { testId, filled: false }; - const locator = page.locator('[data-testid="' + cssEscape(testId) + '"]').first(); + const locator = mdtodoTestIdLocator(testId); await locator.waitFor({ state: "visible", timeout: 10000 }); await locator.fill(value); - return { testId, filled: true, value: opaqueIdSummary(value), valuesRedacted: true }; + const resolvedTestId = await locator.getAttribute("data-testid").catch(() => null); + return { testId: resolvedTestId || mdtodoTestIdCandidates(testId)[0] || null, filled: true, value: opaqueIdSummary(value), valuesRedacted: true }; } async function clickProjectButtonAndMaybeWait(testId, pathPattern) { @@ -1003,7 +1013,7 @@ async function saveMdtodoTaskWithButton(command, type, testId, fields) { if (field.openByDblClickTestId) inlineEditors.push(await ensureMdtodoInlineEditor(field.testId, field.openByDblClickTestId)); if (field.kind === "fill") await fillMdtodoField(field.testId, field.value); if (field.kind === "select") { - const locator = page.locator('[data-testid="' + cssEscape(field.testId) + '"]').first(); + const locator = mdtodoTestIdLocator(field.testId); await locator.waitFor({ state: "visible", timeout: 10000 }); await selectHtmlOptionByValueOrLabel(locator, field.value); } @@ -1023,9 +1033,9 @@ async function clickProjectButtonAndMaybeWaitAny(testIds, pathPattern) { } async function ensureMdtodoInlineEditor(editorTestId, readTestId) { - const editor = page.locator('[data-testid="' + cssEscape(editorTestId) + '"]').first(); + const editor = mdtodoTestIdLocator(editorTestId); if (await visibleLocator(editor)) return { editorTestId, readTestId, opened: false }; - const read = page.locator('[data-testid="' + cssEscape(readTestId) + '"]').first(); + const read = mdtodoTestIdLocator(readTestId); await read.waitFor({ state: "visible", timeout: 10000 }); await read.dblclick(); await editor.waitFor({ state: "visible", timeout: 10000 }); @@ -1041,7 +1051,7 @@ async function editMdtodoTaskTitle(command) { async function editMdtodoTaskBody(command) { const body = commandValue(command, ["text", "body", "value"]); if (!body) throw new Error("editMdtodoTaskBody requires --text or --text-stdin"); - return saveMdtodoTaskWithButton(command, "editMdtodoTaskBody", "mdtodo-edit-body-save", [{ kind: "fill", testId: "mdtodo-edit-body", value: body, openByDblClickTestId: "mdtodo-body-rendered" }]); + return saveMdtodoTaskWithButton(command, "editMdtodoTaskBody", ["mdtodo-body-save", "mdtodo-edit-body-save"], [{ kind: "fill", testId: ["mdtodo-body-editor", "mdtodo-edit-body"], value: body, openByDblClickTestId: ["mdtodo-body-read", "mdtodo-body-rendered"] }]); } async function toggleMdtodoTaskStatus(command) { @@ -1060,7 +1070,7 @@ async function editMdtodoTaskInline(command) { if (field === "body" || field === "content") { const body = commandValue(command, ["body", "text"]); if (!body) throw new Error("editMdtodoTaskInline --field body requires --body, --text, or --text-stdin"); - return saveMdtodoTaskWithButton(command, "editMdtodoTaskInline", "mdtodo-edit-body-save", [{ kind: "fill", testId: "mdtodo-edit-body", value: body, openByDblClickTestId: "mdtodo-body-rendered" }]); + return saveMdtodoTaskWithButton(command, "editMdtodoTaskInline", ["mdtodo-body-save", "mdtodo-edit-body-save"], [{ kind: "fill", testId: ["mdtodo-body-editor", "mdtodo-edit-body"], value: body, openByDblClickTestId: ["mdtodo-body-read", "mdtodo-body-rendered"] }]); } if (field === "status") { const status = commandValue(command, ["status", "text"]); @@ -1219,7 +1229,9 @@ async function deleteMdtodoTask(command) { const selection = await selectTaskIfCommandTargetsOne(command); const firstClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", null); let confirmClick = null; - const confirmVisible = await visibleLocator(page.locator('[data-testid="mdtodo-delete-cancel"]').first()); + const deleteButton = page.locator('[data-testid="mdtodo-delete-task"]').first(); + const confirmVisible = await visibleLocator(page.locator('[data-testid="mdtodo-delete-cancel"]').first()) + || await deleteButton.evaluate((element) => /确认|confirm/iu.test(String(element.textContent || ""))).catch(() => false); if (confirmVisible) confirmClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", /^\/v1\/project-management\/mdtodo\/tasks/u); return { beforeUrl, afterUrl: currentPageUrl(), type: "deleteMdtodoTask", selection, firstClick, confirmClick, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true }; } @@ -1250,7 +1262,7 @@ async function launchWorkbenchFromTask(command) { : null; const providerSelection = await selectMdtodoProviderProfileForLaunch(command); const projectBeforeClick = await projectManagementCommandSnapshot({ includeRaw: true }); - const button = page.locator('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]').first(); + const button = page.locator('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]').first(); await button.waitFor({ state: "visible", timeout: 15000 }); const buttonState = await button.evaluate((element) => ({ disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true", @@ -1392,8 +1404,8 @@ async function projectManagementCommandSnapshot(options = {}) { selected: option.selected === true })) : []; const selectedFileOption = fileOptions.find((option) => option.selected) || null; - const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]'); - const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]'); + const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]'); + const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"], [data-testid="mdtodo-body-read"]'); const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]'); const reportError = document.querySelector('[data-testid="mdtodo-report-error"]'); const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]'); @@ -1410,11 +1422,11 @@ async function projectManagementCommandSnapshot(options = {}) { fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 20), selectedTaskRefRaw: selectedTask?.getAttribute("data-task-ref") || null, selectedTaskId: selectedTask?.getAttribute("data-task-id") || selectedTask?.getAttribute("data-rxx-id") || null, - selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null, + selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || selectedTask?.querySelector("[data-state]")?.getAttribute("data-state") || null, sourceSelectVisible: visible(sourceSelect), fileSelectVisible: visible(fileSelect), sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')), - taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')), + taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"], [data-testid="mdtodo-body-editor"]')), taskBodyVisible: visible(bodyRendered), taskBodyText: visible(bodyRendered) ? text(bodyRendered) : "", newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')), @@ -1427,7 +1439,7 @@ async function projectManagementCommandSnapshot(options = {}) { launchButtonVisible: visible(launch), launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true", launchButtonText: text(launch), - blockerTexts: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [role="alert"]')).filter(visible).map(text).filter(Boolean).slice(0, 8), + blockerTexts: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [data-testid="mdtodo-create-blocker"], .launch-blocker, [role="alert"]')).filter(visible).map(text).filter(Boolean).slice(0, 8), workbenchLinkCount: Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible).length, valuesRedacted: true };