diff --git a/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml index 4c31112b..46c6a780 100644 --- a/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml +++ b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml @@ -23,16 +23,20 @@ sentinel: path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_0db4dc4e46adf188/tasks/R1 - type: screenshot label: mdtodo-desktop-few-task-gap + waitProjectManagementReady: true - type: goto path: /projects/mdtodo/sources/constart-71freq-mdtodo/files/file_5f9645ffe8774b92/tasks/R14 - type: screenshot label: mdtodo-r14-selected + waitProjectManagementReady: true - type: openMdtodoReportPreview task: R14 link: R14 - type: screenshot label: mdtodo-r14-report-preview + waitProjectManagementReady: true - type: toggleMdtodoReportFullscreen text: toggle - type: screenshot label: mdtodo-r14-report-fullscreen + waitProjectManagementReady: true diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index d581e797..c2ef87f5 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -390,7 +390,7 @@ async function processCommand(command) { case "deleteMdtodoTask": return deleteMdtodoTask(command); case "launchWorkbenchFromTask": return withObserverSync(await launchWorkbenchFromTask(command), "launchWorkbenchFromTask"); case "launchWorkbenchFromMdtodo": return withObserverSync(await launchWorkbenchFromMdtodo(command), "launchWorkbenchFromMdtodo"); - case "screenshot": return captureScreenshot(command.reason || "manual", command.imageType || "png"); + case "screenshot": return captureCommandScreenshot(command); case "mark": return { mark: truncate(command.label || command.text || "mark", 200), currentUrl: currentPageUrl(), pageId }; case "stop": stopping = true; return { stopping: true, currentUrl: currentPageUrl(), pageId }; default: throw new Error("unsupported observer command type: " + command.type); @@ -1251,6 +1251,28 @@ async function projectManagementReadinessSnapshot(targetPage) { }, { selectors }).catch((error) => ({ error: errorSummary(error), valuesRedacted: true })); } +async function waitForProjectManagementCommandReady(options = {}) { + const timeoutMs = Number.isFinite(Number(options.timeoutMs)) ? Math.max(1, Number(options.timeoutMs)) : 15000; + const started = Date.now(); + const deadline = started + timeoutMs; + let last = null; + while (Date.now() <= deadline) { + last = await projectManagementCommandSnapshot(); + const path = String(last?.path || safeUrlPath(currentPageUrl()) || ""); + const baseReady = last?.pageKind === "project-management-mdtodo" + && Number(last?.sourceCount || 0) > 0 + && Number(last?.fileCount || 0) > 0 + && Number(last?.taskCount || 0) > 0; + const needsTask = /\/tasks\//u.test(path); + const taskReady = !needsTask || Boolean(last?.selectedTaskId || last?.selectedTaskRef?.hash || last?.taskBodyVisible === true || last?.launchButtonVisible === true); + const needsReport = /\/reports\//u.test(path); + const reportReady = !needsReport || last?.reportPreviewVisible === true || last?.reportFullscreenVisible === true || Number(last?.reportLinkCount || 0) > 0; + if (baseReady && taskReady && reportReady) return { ok: true, reason: "project-management-command-ready", durationMs: Date.now() - started, snapshot: last, valuesRedacted: true }; + await page.waitForTimeout(250).catch(() => {}); + } + return { ok: false, reason: "project-management-command-not-ready", durationMs: Date.now() - started, snapshot: last, valuesRedacted: true }; +} + function isWorkbenchPathname(value) { const pathname = String(value || ""); return pathname === "/workbench" || pathname === "/workspace" || pathname.startsWith("/workbench/") || pathname.startsWith("/workspace/"); @@ -3793,6 +3815,18 @@ async function captureScreenshot(reason, imageType = "png") { return artifact; } +async function captureCommandScreenshot(command) { + const shouldWaitProject = command.waitProjectManagementReady === true; + const readiness = shouldWaitProject ? await waitForProjectManagementCommandReady({ timeoutMs: 15000 }) : null; + if (readiness && readiness.ok !== true) { + const error = new Error("screenshot project-management readiness wait failed: " + (readiness.reason || "not-ready")); + error.details = { readiness, currentUrl: currentPageUrl(), pageId, valuesRedacted: true }; + throw error; + } + const artifact = await captureScreenshot(command.reason || command.label || "manual", command.imageType || "png"); + return { ...artifact, readiness, valuesRedacted: true }; +} + function eventRecord(type, data) { const clean = sanitize(data) || {}; return { ts: new Date().toISOString(), type, jobId, pageId: clean.pageId ?? pageId, pageRole: clean.pageRole ?? "control", sampleSeq, commandId: activeCommandId, ...clean }; @@ -3838,6 +3872,7 @@ function commandInputSummary(command) { expectedSentinelRange: command.expectedSentinelRange || null, expectedActionWaitMs: command.expectedActionWaitMs === null || command.expectedActionWaitMs === undefined || command.expectedActionWaitMs === "" ? null : Number(command.expectedActionWaitMs), requireComposerReady: command.requireComposerReady === true, + waitProjectManagementReady: command.waitProjectManagementReady === true, findingId: command.findingId || null, blocking: command.blocking === true ? true : command.blocking === false ? false : null, sourceId: opaque(command.sourceId), diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index ac47490e..d1adf2a8 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2574,6 +2574,7 @@ function appendScenarioObserveCommandArgs(args: string[], item: Record