diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index c2ef87f5..719c8c8f 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -1266,7 +1266,7 @@ async function waitForProjectManagementCommandReady(options = {}) { 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; + const reportReady = !needsReport || last?.reportPreviewVisible === true || last?.reportFullscreenVisible === true; 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(() => {}); } @@ -2744,19 +2744,53 @@ async function openMdtodoReportPreview(command) { error.details = { beforeUrl, linkState, beforeProject, valuesRedacted: true }; throw error; } - const responsePromise = page.waitForResponse((response) => { + const attempts = []; + let response = null; + let afterProject = null; + for (let attempt = 1; attempt <= 3; attempt += 1) { + const responsePromise = page.waitForResponse((item) => { + try { + const request = item.request(); + return request.method().toUpperCase() === "GET" && new URL(item.url()).pathname === "/v1/project-management/mdtodo/report-preview"; + } catch { + return false; + } + }, { timeout: 10000 }).then((item) => ({ observed: true, status: item.status(), path: new URL(item.url()).pathname })).catch((error) => ({ observed: false, waitError: errorSummary(error) })); + const clickMode = attempt === 1 ? "normal" : "force"; + let click = { ok: true, mode: clickMode, valuesRedacted: true }; + await link.scrollIntoViewIfNeeded().catch(() => null); try { - const request = response.request(); - return request.method().toUpperCase() === "GET" && new URL(response.url()).pathname === "/v1/project-management/mdtodo/report-preview"; - } catch { - return false; + if (attempt === 1) await link.click(); + else await link.click({ force: true, timeout: 5000 }); + } catch (error) { + click = { ok: false, mode: clickMode, error: errorSummary(error), valuesRedacted: true }; + await link.evaluate((element) => element.click()).catch(() => null); } - }, { timeout: 30000 }).then((response) => ({ observed: true, status: response.status(), path: new URL(response.url()).pathname })).catch((error) => ({ observed: false, waitError: errorSummary(error) })); - await link.scrollIntoViewIfNeeded().catch(() => null); - await link.click(); - const response = await responsePromise; - await page.locator('[data-testid="mdtodo-report-preview"], [data-testid="mdtodo-report-error"]').first().waitFor({ state: "visible", timeout: 10000 }).catch(() => null); - return { beforeUrl, afterUrl: currentPageUrl(), type: "openMdtodoReportPreview", selection, linkState, response, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true }; + await page.locator('[data-testid="mdtodo-report-preview"], [data-testid="mdtodo-report-error"]').first().waitFor({ state: "visible", timeout: 5000 }).catch(() => null); + afterProject = await projectManagementCommandSnapshot(); + response = afterProject?.reportPreviewVisible === true + ? { observed: null, skippedAfterPreviewVisible: true, valuesRedacted: true } + : await responsePromise; + attempts.push({ + attempt, + click, + response, + afterUrl: currentPageUrl(), + reportPreviewVisible: afterProject?.reportPreviewVisible === true, + reportErrorVisible: afterProject?.reportErrorVisible === true, + reportFullscreenVisible: afterProject?.reportFullscreenVisible === true, + valuesRedacted: true + }); + if (afterProject?.reportPreviewVisible === true) break; + await page.waitForTimeout(700).catch(() => null); + } + afterProject = afterProject ?? await projectManagementCommandSnapshot(); + if (afterProject?.reportPreviewVisible !== true) { + const error = new Error("openMdtodoReportPreview did not show markdown report preview"); + error.details = { beforeUrl, afterUrl: currentPageUrl(), selection, linkState, response, attempts, beforeProject, afterProject, pageId, valuesRedacted: true }; + throw error; + } + return { beforeUrl, afterUrl: currentPageUrl(), type: "openMdtodoReportPreview", selection, linkState, response, attempts, beforeProject, afterProject, pageId, valuesRedacted: true }; } async function toggleMdtodoReportFullscreen(command) { @@ -2977,6 +3011,7 @@ async function projectManagementCommandSnapshot(options = {}) { const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]'); const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]'); 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"]'); const reportLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-report-link"]')).filter(visible); return { @@ -3002,6 +3037,8 @@ async function projectManagementCommandSnapshot(options = {}) { reportLinkCount: reportLinks.length, reportPreviewVisible: visible(reportPreview), reportPreviewText: visible(reportPreview) ? text(reportPreview) : "", + reportErrorVisible: visible(reportError), + reportErrorText: visible(reportError) ? text(reportError) : "", reportFullscreenVisible: visible(reportFullscreen), launchButtonVisible: visible(launch), launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true", @@ -3044,6 +3081,8 @@ function sanitizeProjectCommandSnapshot(value) { taskBody: value.taskBodyText ? textDigest(value.taskBodyText, 200) : null, reportPreviewText: undefined, reportPreview: value.reportPreviewText ? textDigest(value.reportPreviewText, 200) : null, + reportErrorText: undefined, + reportError: value.reportErrorText ? textDigest(value.reportErrorText, 200) : null, launchButtonTextHash: value.launchButtonText ? sha256Text(value.launchButtonText) : null, launchButtonTextPreview: value.launchButtonText ? truncate(value.launchButtonText, 80) : null, launchButtonText: undefined,