diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index 4dba9618..0d46bee2 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -446,6 +446,22 @@ async function runLiveSmoke(args) { }); } + const help = http.ok ? await inspectLiveHelpRoute(args.url) : { status: "skip", summary: "Browser Markdown help check skipped because HTTP fetch failed.", evidence: [] }; + addCheck(checks, blockers, "live-help-markdown-route", help.status, help.summary, { + blocker: help.status === "pass" ? null : "runtime_blocker", + evidence: help.evidence, + observations: help.observations + }); + + if (help.status === "skip") { + blockers.push({ + type: "observability_blocker", + scope: "live-help-markdown-route", + status: "open", + summary: help.summary + }); + } + const journey = http.ok ? await inspectLiveUserJourney(args.url) : { status: "skip", summary: "Browser Code Agent journey skipped because HTTP fetch failed.", evidence: [] }; addCheck(checks, blockers, "live-code-agent-browser-journey", journey.status, journey.summary, { blocker: journey.status === "pass" ? null : "runtime_blocker", @@ -1450,7 +1466,7 @@ async function inspectLiveDom(url) { browser = await chromium.launch({ headless: true }); const page = await browser.newPage({ viewport: { width: 1366, height: 768 } }); await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 }); - const dom = await page.evaluate(() => { + const dom = await page.evaluate(async () => { const workspace = document.querySelector('[data-view="workspace"]'); const gate = document.querySelector('[data-view="gate"]'); const help = document.querySelector('[data-view="help"]'); @@ -1465,6 +1481,15 @@ async function inspectLiveDom(url) { const box = element.getBoundingClientRect(); return style.visibility !== "hidden" && style.display !== "none" && box.width > 0 && box.height > 0; }; + window.scrollTo(0, 240); + document.documentElement.scrollTop = 240; + document.body.scrollTop = 240; + await new Promise((resolve) => requestAnimationFrame(resolve)); + const rootAfterScrollAttempt = { + windowScrollY: window.scrollY, + htmlScrollTop: document.documentElement.scrollTop, + bodyScrollTop: document.body.scrollTop + }; return { title: document.title, bodyOverflow: bodyStyle.overflow, @@ -1474,6 +1499,7 @@ async function inspectLiveDom(url) { helpHidden: help ? help.hidden : null, diagnosticsHidden: diagnostics ? diagnostics.hidden : null, outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2, + rootAfterScrollAttempt, labelsPresent: ["硬件资源", "Agent 对话", "工作清单", "可信记录", "使用说明"].every((label) => text.includes(label)), coreControlsVisible: { commandInput: visible("#command-input"), @@ -1495,6 +1521,9 @@ async function inspectLiveDom(url) { (dom.helpHidden === true || dom.helpHidden === null) && (dom.diagnosticsHidden === true || dom.diagnosticsHidden === null) && dom.outerScrollLocked && + dom.rootAfterScrollAttempt.windowScrollY === 0 && + dom.rootAfterScrollAttempt.htmlScrollTop === 0 && + dom.rootAfterScrollAttempt.bodyScrollTop === 0 && dom.labelsPresent && Object.values(dom.coreControlsVisible).every(Boolean); return { @@ -1514,6 +1543,51 @@ async function inspectLiveDom(url) { } } +async function inspectLiveHelpRoute(url) { + let chromium; + try { + ({ chromium } = await import("playwright")); + } catch (error) { + return { + status: "skip", + summary: `Browser Markdown help check skipped because Playwright is unavailable: ${error.message}`, + evidence: ["playwright import failed"] + }; + } + + let browser; + try { + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1366, height: 768 } }); + await page.goto(url, { waitUntil: "domcontentloaded", timeout: 12000 }); + await page.locator('[data-route="help"]').click(); + await page.waitForFunction(() => document.querySelector("#help-content")?.dataset.helpState === "ready", null, { timeout: 8000 }); + const help = await inspectHelpMarkdownRoute(page); + const pass = help.nonDefaultMarkdownHelp && help.termsPresent; + return { + status: pass ? "pass" : "blocked", + summary: pass + ? "Desktop browser help route remains non-default, markdown-rendered, and scrollable." + : "Desktop browser help route did not satisfy the markdown workbench contract.", + evidence: [ + `hash=${help.hash ?? "unknown"}`, + `heading=${help.heading ?? "unknown"}`, + `rootStillLocked=${help.rootStillLocked}`, + `helpPanelScrolls=${help.helpPanelScrolls}` + ], + observations: help + }; + } catch (error) { + return { + status: "blocked", + summary: `Desktop browser help route failed: ${error.message}`, + evidence: ["browser navigation or markdown render failed"] + }; + } finally { + if (browser) await browser.close(); + } +} + async function inspectLiveUserJourney(url) { let chromium; try { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 6d3191c8..662d9102 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -60,21 +60,27 @@ const requiredTrustedRecordTerms = Object.freeze([ ]); const workbenchSmoke = runDevCloudWorkbenchStaticSmoke(); const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract"); +const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract"); const mobileLayoutSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "mobile-workbench-layout-contract"); const conversationUxSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "code-agent-conversation-ux-states"); const mobileWorkbenchSmoke = await runDevCloudWorkbenchMobileSmoke(); +const mobileOuterScrollSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-outer-scroll-lock"); +const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.id === "mobile-help-markdown-route"); const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke(); assert.equal(workbenchSmoke.status, "pass", JSON.stringify(workbenchSmoke.blockers, null, 2)); assert.equal(workbenchSmoke.evidenceLevel, "SOURCE"); assert.equal(workbenchSmoke.devLive, false); assert.equal(helpSmokeCheck?.status, "pass", "PR #114 help Markdown route must be present and ready"); +assert.equal(outerScrollSmokeCheck?.status, "pass", "desktop workbench shell must lock outer page scrolling"); assert.equal(mobileLayoutSmokeCheck?.status, "pass", "mobile 390x844 workbench layout contract must be present"); assert.equal(conversationUxSmokeCheck?.status, "pass", "Code Agent conversation UX states must remain distinct"); assert.equal(workbenchSmoke.help.status, "pass", "smoke report must not leave #114 help contract pending"); assert.notEqual(mobileWorkbenchSmoke.status, "blocked", JSON.stringify(mobileWorkbenchSmoke.blockers, null, 2)); assert.equal(mobileWorkbenchSmoke.evidenceLevel, "SOURCE"); assert.equal(mobileWorkbenchSmoke.devLive, false); +assert.equal(mobileOuterScrollSmokeCheck?.status, "pass", "mobile smoke must prove outer page scroll remains locked"); +assert.equal(mobileHelpSmokeCheck?.status, "pass", "mobile help route must remain non-default Markdown content"); assert.equal(localAgentFixtureSmoke.status, "pass", JSON.stringify(localAgentFixtureSmoke.blockers, null, 2)); assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE"); assert.equal(localAgentFixtureSmoke.devLive, false);